@lppx/lanshare 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.handleStartCommand = handleStartCommand;
40
40
  const prompts_1 = __importDefault(require("prompts"));
41
+ const open_1 = __importDefault(require("open"));
41
42
  const index_1 = require("../index");
42
43
  const logger_1 = __importStar(require("../utils/logger"));
43
44
  const network_1 = require("./network");
@@ -123,6 +124,15 @@ async function handleStartCommand() {
123
124
  ip: ipResponse.ip,
124
125
  port: port
125
126
  });
127
+ // 自动打开浏览器
128
+ const url = `http://${ipResponse.ip}:${port}`;
129
+ try {
130
+ await (0, open_1.default)(url);
131
+ logger_1.default.info(`已自动打开浏览器: ${url}`);
132
+ }
133
+ catch (error) {
134
+ logger_1.default.warn('自动打开浏览器失败,请手动访问:', url);
135
+ }
126
136
  }
127
137
  catch (error) {
128
138
  logger_1.default.error('启动失败', error);
package/dist/src/cli.js CHANGED
File without changes
@@ -22,7 +22,18 @@ function createFilesRouter(uploadDir) {
22
22
  logger_1.default.error('读取文件列表失败', err);
23
23
  return res.status(500).json({ error: '读取文件列表失败' });
24
24
  }
25
- const fileDetails = files.map(filename => {
25
+ // 过滤掉临时上传目录和非文件项
26
+ const fileDetails = files
27
+ .filter(filename => {
28
+ // 排除临时上传目录
29
+ if (filename === '.uploading')
30
+ return false;
31
+ const filepath = path_1.default.join(uploadDir, filename);
32
+ const stats = fs_1.default.statSync(filepath);
33
+ // 只返回文件,不返回目录
34
+ return stats.isFile();
35
+ })
36
+ .map(filename => {
26
37
  const filepath = path_1.default.join(uploadDir, filename);
27
38
  const stats = fs_1.default.statSync(filepath);
28
39
  return {
@@ -34,7 +45,7 @@ function createFilesRouter(uploadDir) {
34
45
  createdAt: stats.birthtime
35
46
  };
36
47
  });
37
- logger_1.default.debug(`获取文件列表: ${files.length} 个文件`);
48
+ logger_1.default.debug(`获取文件列表: ${fileDetails.length} 个文件`);
38
49
  res.json(fileDetails);
39
50
  });
40
51
  });
@@ -11,24 +11,22 @@ const fs_1 = __importDefault(require("fs"));
11
11
  const logger_1 = __importDefault(require("../utils/logger"));
12
12
  function createUploadRouter(uploadDir) {
13
13
  const router = (0, express_1.Router)();
14
+ // #region 配置临时上传目录
15
+ const tempDir = path_1.default.join(uploadDir, '.uploading');
16
+ if (!fs_1.default.existsSync(tempDir)) {
17
+ fs_1.default.mkdirSync(tempDir, { recursive: true });
18
+ }
19
+ // #endregion
14
20
  // #region 配置文件上传
15
21
  const storage = multer_1.default.diskStorage({
16
22
  destination: (_req, _file, cb) => {
17
- cb(null, uploadDir);
23
+ // 先上传到临时目录
24
+ cb(null, tempDir);
18
25
  },
19
26
  filename: (_req, file, cb) => {
20
- // 保留原始文件名,如果文件已存在则添加数字后缀
21
- let filename = file.originalname;
22
- let filepath = path_1.default.join(uploadDir, filename);
23
- let counter = 1;
24
- while (fs_1.default.existsSync(filepath)) {
25
- const ext = path_1.default.extname(file.originalname);
26
- const basename = path_1.default.basename(file.originalname, ext);
27
- filename = `${basename} (${counter})${ext}`;
28
- filepath = path_1.default.join(uploadDir, filename);
29
- counter++;
30
- }
31
- cb(null, filename);
27
+ // 保留原始文件名
28
+ file.originalname = Buffer.from(file.originalname, 'latin1').toString('utf8');
29
+ cb(null, file.originalname);
32
30
  }
33
31
  });
34
32
  const upload = (0, multer_1.default)({ storage });
@@ -39,8 +37,30 @@ function createUploadRouter(uploadDir) {
39
37
  logger_1.default.warn('上传失败: 没有文件');
40
38
  return res.status(400).json({ error: '没有文件上传' });
41
39
  }
42
- logger_1.default.info(`文件上传成功: ${_req.file.originalname} -> ${_req.file.filename} (${_req.file.size} bytes)`);
43
- res.json({ message: '文件上传成功', filename: _req.file.filename });
40
+ // 上传完成后,从临时目录移动到正式目录
41
+ const tempPath = _req.file.path;
42
+ let filename = _req.file.originalname;
43
+ let finalPath = path_1.default.join(uploadDir, filename);
44
+ let counter = 1;
45
+ // 如果文件已存在则添加数字后缀
46
+ while (fs_1.default.existsSync(finalPath)) {
47
+ const ext = path_1.default.extname(_req.file.originalname);
48
+ const basename = path_1.default.basename(_req.file.originalname, ext);
49
+ filename = `${basename} (${counter})${ext}`;
50
+ finalPath = path_1.default.join(uploadDir, filename);
51
+ counter++;
52
+ }
53
+ // 移动文件到正式目录
54
+ fs_1.default.rename(tempPath, finalPath, (err) => {
55
+ if (err) {
56
+ logger_1.default.error(`移动文件失败: ${_req.file.originalname}`, err);
57
+ // 清理临时文件
58
+ fs_1.default.unlink(tempPath, () => { });
59
+ return res.status(500).json({ error: '文件保存失败' });
60
+ }
61
+ logger_1.default.info(`文件上传成功: ${_req.file.originalname} -> ${filename} (${_req.file.size} bytes)`);
62
+ res.json({ message: '文件上传成功', filename: filename });
63
+ });
44
64
  });
45
65
  // #endregion
46
66
  return router;
package/package.json CHANGED
@@ -3,8 +3,8 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.0.1",
7
- "description": "ts test",
6
+ "version": "1.0.3",
7
+ "description": "这是一个局域网分享工具",
8
8
  "bin": {
9
9
  "lanshare": "dist/src/cli.js",
10
10
  "lsh": "dist/src/cli.js"
@@ -29,6 +29,7 @@
29
29
  "commander": "^12.0.0",
30
30
  "express": "^4.18.2",
31
31
  "multer": "^1.4.5-lts.1",
32
+ "open": "^11.0.0",
32
33
  "prompts": "^2.4.2",
33
34
  "socket.io": "^4.8.3",
34
35
  "winston": "^3.19.0"
@@ -56,6 +56,8 @@
56
56
  display: flex;
57
57
  align-items: center;
58
58
  gap: 12px;
59
+ min-width: 0;
60
+ overflow: hidden;
59
61
  }
60
62
 
61
63
  .file-icon {
@@ -86,6 +88,8 @@
86
88
 
87
89
  .file-details {
88
90
  min-width: 0;
91
+ flex: 1;
92
+ overflow: hidden;
89
93
  }
90
94
 
91
95
  .file-name {
@@ -96,6 +100,7 @@
96
100
  white-space: nowrap;
97
101
  overflow: hidden;
98
102
  text-overflow: ellipsis;
103
+ max-width: 100%;
99
104
  }
100
105
 
101
106
  .file-meta {
@@ -165,3 +170,96 @@
165
170
  background: #ffebee;
166
171
  color: #d32f2f;
167
172
  }
173
+
174
+ .upload-progress-section {
175
+ background: white;
176
+ border-radius: 12px;
177
+ padding: 16px;
178
+ margin-bottom: 20px;
179
+ }
180
+
181
+ .upload-item {
182
+ display: flex;
183
+ align-items: center;
184
+ gap: 12px;
185
+ padding: 12px;
186
+ background: #f0f8ff;
187
+ border: 1px solid #d0e8ff;
188
+ border-radius: 8px;
189
+ margin-bottom: 8px;
190
+ }
191
+
192
+ .upload-item:last-child {
193
+ margin-bottom: 0;
194
+ }
195
+
196
+ .upload-icon {
197
+ width: 40px;
198
+ height: 40px;
199
+ border-radius: 8px;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ font-size: 20px;
204
+ flex-shrink: 0;
205
+ background: #e3f2fd;
206
+ }
207
+
208
+ .upload-details {
209
+ flex: 1;
210
+ min-width: 0;
211
+ }
212
+
213
+ .upload-filename {
214
+ font-size: 14px;
215
+ font-weight: 500;
216
+ color: #1a1a1a;
217
+ margin-bottom: 6px;
218
+ white-space: nowrap;
219
+ overflow: hidden;
220
+ text-overflow: ellipsis;
221
+ }
222
+
223
+ .upload-progress-bar {
224
+ width: 100%;
225
+ height: 8px;
226
+ background: #e0e0e0;
227
+ border-radius: 4px;
228
+ overflow: hidden;
229
+ margin-bottom: 4px;
230
+ }
231
+
232
+ .upload-progress-fill {
233
+ height: 100%;
234
+ background: linear-gradient(90deg, #4A90E2, #5BA3F5);
235
+ border-radius: 4px;
236
+ transition: width 0.3s ease;
237
+ }
238
+
239
+ .upload-info {
240
+ display: flex;
241
+ justify-content: space-between;
242
+ font-size: 12px;
243
+ color: #666;
244
+ }
245
+
246
+ .upload-percent {
247
+ color: #4A90E2;
248
+ font-weight: 600;
249
+ }
250
+
251
+ .uploading-spinner {
252
+ width: 20px;
253
+ height: 20px;
254
+ border: 2px solid #e0e0e0;
255
+ border-top-color: #4A90E2;
256
+ border-radius: 50%;
257
+ animation: spin 0.8s linear infinite;
258
+ flex-shrink: 0;
259
+ }
260
+
261
+ @keyframes spin {
262
+ to {
263
+ transform: rotate(360deg);
264
+ }
265
+ }
package/public/index.html CHANGED
@@ -4,6 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>LAN Share Control</title>
7
+ <!-- 样式表引入 -->
7
8
  <link rel="stylesheet" href="/css/base.css">
8
9
  <link rel="stylesheet" href="/css/header.css">
9
10
  <link rel="stylesheet" href="/css/drop-zone.css">
@@ -12,8 +13,11 @@
12
13
  <link rel="stylesheet" href="/css/responsive.css">
13
14
  </head>
14
15
  <body>
16
+ <!-- 页面头部 -->
15
17
  <header class="header">
18
+ <!-- 头部左侧:Logo 和标题 -->
16
19
  <div class="header-left">
20
+ <!-- 分享图标 -->
17
21
  <svg class="share-icon" width="24" height="24" viewBox="0 0 24 24" fill="none">
18
22
  <path d="M18 8C19.6569 8 21 6.65685 21 5C21 3.34315 19.6569 2 18 2C16.3431 2 15 3.34315 15 5C15 6.65685 16.3431 8 18 8Z" stroke="currentColor" stroke-width="2"/>
19
23
  <path d="M6 15C7.65685 15 9 13.6569 9 12C9 10.3431 7.65685 9 6 9C4.34315 9 3 10.3431 3 12C3 13.6569 4.34315 15 6 15Z" stroke="currentColor" stroke-width="2"/>
@@ -22,7 +26,9 @@
22
26
  </svg>
23
27
  <h1 class="title">LAN Share Control</h1>
24
28
  </div>
29
+ <!-- 头部右侧:IP 地址和用户头像 -->
25
30
  <div class="header-right">
31
+ <!-- 显示当前服务器 IP 地址 -->
26
32
  <span class="ip-address" id="ipAddress">
27
33
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
28
34
  <rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2"/>
@@ -31,6 +37,7 @@
31
37
  </svg>
32
38
  <span id="ipText">192.168.1.15</span>
33
39
  </span>
40
+ <!-- 用户头像 -->
34
41
  <div class="user-avatar">
35
42
  <svg width="32" height="32" viewBox="0 0 32 32">
36
43
  <circle cx="16" cy="16" r="16" fill="#FF9800"/>
@@ -41,10 +48,14 @@
41
48
  </div>
42
49
  </header>
43
50
 
51
+ <!-- 主容器 -->
44
52
  <div class="main-container">
53
+ <!-- 左侧面板:文件上传和分享列表 -->
45
54
  <div class="left-panel">
55
+ <!-- 文件拖放上传区域 -->
46
56
  <div class="drop-zone" id="dropZone">
47
57
  <div class="drop-zone-icon">
58
+ <!-- 云上传图标 -->
48
59
  <svg width="64" height="64" viewBox="0 0 24 24" fill="none">
49
60
  <path d="M7 18C4.79086 18 3 16.2091 3 14C3 11.7909 4.79086 10 7 10C7.17822 10 7.35381 10.0104 7.52614 10.0305C7.84294 7.14916 10.2207 5 13 5C15.7793 5 18.1571 7.14916 18.4739 10.0305C18.6462 10.0104 18.8218 10 19 10C21.2091 10 23 11.7909 23 14C23 16.2091 21.2091 18 19 18H7Z" stroke="#4A90E2" stroke-width="2"/>
50
61
  <path d="M12 12V19M12 12L9 15M12 12L15 15" stroke="#4A90E2" stroke-width="2" stroke-linecap="round"/>
@@ -52,41 +63,60 @@
52
63
  </div>
53
64
  <h2 class="drop-zone-title">Drag & Drop files here</h2>
54
65
  <p class="drop-zone-subtitle">or click to browse your computer</p>
66
+ <!-- 隐藏的文件选择输入框 -->
55
67
  <input type="file" id="fileInput" multiple hidden>
56
68
  </div>
57
69
 
70
+ <!-- 上传进度区域 -->
71
+ <div class="upload-progress-section" id="uploadProgressSection" style="display: none;"></div>
72
+
73
+ <!-- 活动分享文件列表 -->
58
74
  <div class="active-shares">
59
75
  <div class="section-header">
60
76
  <h3>Active Shares</h3>
77
+ <!-- 文件数量统计 -->
61
78
  <span class="file-count" id="fileCount">0 Files</span>
62
79
  </div>
80
+ <!-- 文件列表表格 -->
63
81
  <div class="shares-table">
82
+ <!-- 表头 -->
64
83
  <div class="table-header">
65
84
  <div class="col-name">FILE NAME</div>
66
85
  <div class="col-size">SIZE</div>
67
86
  <div class="col-downloads">DOWNLOADS</div>
68
87
  <div class="col-actions">ACTIONS</div>
69
88
  </div>
89
+ <!-- 表格内容区域,由 JS 动态填充 -->
70
90
  <div class="table-body" id="fileList"></div>
71
91
  </div>
72
92
  </div>
73
93
  </div>
74
94
 
95
+ <!-- 右侧面板:已连接设备列表 -->
75
96
  <div class="right-panel">
76
97
  <div class="connected-devices">
77
98
  <h3>Connected Devices</h3>
78
99
  <p class="devices-subtitle">Devices currently accessing shared files</p>
100
+ <!-- 设备列表容器,由 JS 动态填充 -->
79
101
  <div class="devices-list" id="devicesList"></div>
80
102
  </div>
81
103
  </div>
82
104
  </div>
83
105
 
106
+ <!-- JavaScript 脚本引入 -->
107
+ <!-- Socket.IO 客户端库 -->
84
108
  <script src="/socket.io/socket.io.js"></script>
109
+ <!-- WebSocket 连接管理 -->
85
110
  <script src="/js/socket.js"></script>
111
+ <!-- 文件上传功能 -->
86
112
  <script src="/js/upload.js"></script>
113
+ <!-- 文件列表管理 -->
87
114
  <script src="/js/file-list.js"></script>
115
+ <!-- 设备列表管理 -->
88
116
  <script src="/js/device-list.js"></script>
117
+ <!-- API 接口调用 -->
89
118
  <script src="/js/api.js"></script>
119
+ <!-- 主程序入口 -->
90
120
  <script src="/js/main.js"></script>
91
121
  </body>
92
122
  </html>
@@ -1,4 +1,6 @@
1
1
  // #region 文件上传处理
2
+ window.uploadingFiles = new Map();
3
+
2
4
  function initUpload() {
3
5
  const fileInput = document.getElementById('fileInput');
4
6
  const dropZone = document.getElementById('dropZone');
@@ -31,19 +33,99 @@ function initUpload() {
31
33
 
32
34
  async function uploadFiles(files) {
33
35
  for (let file of files) {
34
- const formData = new FormData();
35
- formData.append('file', file);
36
- try {
37
- const response = await fetch('/upload', { method: 'POST', body: formData });
38
- if (response.ok) {
36
+ await uploadFile(file);
37
+ }
38
+ }
39
+
40
+ async function uploadFile(file) {
41
+ const formData = new FormData();
42
+ formData.append('file', file);
43
+
44
+ const uploadId = Date.now() + '_' + file.name;
45
+ window.uploadingFiles.set(uploadId, {
46
+ name: file.name,
47
+ size: file.size,
48
+ progress: 0,
49
+ loaded: 0
50
+ });
51
+
52
+ renderUploadProgress();
53
+
54
+ try {
55
+ const xhr = new XMLHttpRequest();
56
+
57
+ xhr.upload.addEventListener('progress', (e) => {
58
+ if (e.lengthComputable) {
59
+ const progress = (e.loaded / e.total) * 100;
60
+ const uploadInfo = window.uploadingFiles.get(uploadId);
61
+ if (uploadInfo) {
62
+ uploadInfo.progress = progress;
63
+ uploadInfo.loaded = e.loaded;
64
+ renderUploadProgress();
65
+ }
66
+ }
67
+ });
68
+
69
+ xhr.addEventListener('load', () => {
70
+ if (xhr.status === 200) {
39
71
  console.log('文件上传成功:', file.name);
72
+ window.uploadingFiles.delete(uploadId);
73
+ renderUploadProgress();
40
74
  loadFiles();
41
75
  } else {
42
76
  console.error('文件上传失败:', file.name);
77
+ window.uploadingFiles.delete(uploadId);
78
+ renderUploadProgress();
43
79
  }
44
- } catch (error) {
45
- console.error('上传出错:', error);
46
- }
80
+ });
81
+
82
+ xhr.addEventListener('error', () => {
83
+ console.error('上传出错:', file.name);
84
+ window.uploadingFiles.delete(uploadId);
85
+ renderUploadProgress();
86
+ });
87
+
88
+ xhr.open('POST', '/upload');
89
+ xhr.send(formData);
90
+ } catch (error) {
91
+ console.error('上传出错:', error);
92
+ window.uploadingFiles.delete(uploadId);
93
+ renderUploadProgress();
94
+ }
95
+ }
96
+
97
+ function renderUploadProgress() {
98
+ const section = document.getElementById('uploadProgressSection');
99
+ const uploadingArray = Array.from(window.uploadingFiles.values());
100
+
101
+ if (uploadingArray.length === 0) {
102
+ section.style.display = 'none';
103
+ section.innerHTML = '';
104
+ return;
47
105
  }
106
+
107
+ section.style.display = 'block';
108
+
109
+ section.innerHTML = uploadingArray.map(upload => {
110
+ const progressPercent = Math.round(upload.progress);
111
+ const sizeText = `${formatFileSize(upload.loaded)} / ${formatFileSize(upload.size)}`;
112
+
113
+ return `
114
+ <div class="upload-item">
115
+ <div class="upload-icon">📤</div>
116
+ <div class="upload-details">
117
+ <div class="upload-filename">${upload.name}</div>
118
+ <div class="upload-progress-bar">
119
+ <div class="upload-progress-fill" style="width: ${progressPercent}%"></div>
120
+ </div>
121
+ <div class="upload-info">
122
+ <span class="upload-size">${sizeText}</span>
123
+ <span class="upload-percent">${progressPercent}%</span>
124
+ </div>
125
+ </div>
126
+ <div class="uploading-spinner"></div>
127
+ </div>
128
+ `;
129
+ }).join('');
48
130
  }
49
131
  // #endregion