1- import { readFileSync , existsSync , readdirSync , statSync } from 'fs' ;
1+ import { readFileSync , existsSync , readdirSync , statSync , unlinkSync } from 'fs' ;
22import { createReadStream } from 'fs' ;
33import { execSync } from 'child_process' ;
44import path from 'path' ;
@@ -53,7 +53,7 @@ const findLatestPackage = (projectDir, packageName) => {
5353 . map ( file => {
5454 const filePath = path . join ( parentDir , file ) ;
5555 const stats = statSync ( filePath ) ;
56- return { file, filePath, mtime : stats . mtime } ;
56+ return { file, filePath, mtime : stats . mtime , size : stats . size } ;
5757 } )
5858 . sort ( ( a , b ) => b . mtime - a . mtime ) ;
5959
@@ -170,6 +170,16 @@ export default (fastify, options, done) => {
170170 }
171171
172172 const projectName = path . basename ( projectRootDir ) ;
173+ const templatePath = path . join ( projectRootDir , 'public' , 'download.html' ) ;
174+
175+ if ( ! existsSync ( templatePath ) ) {
176+ return reply . code ( 500 ) . send ( {
177+ success : false ,
178+ message : '下载页面模板不存在' ,
179+ } ) ;
180+ }
181+
182+ let html = readFileSync ( templatePath , 'utf-8' ) ;
173183
174184 const files = [
175185 { name : `${ projectName } .7z` , desc : '7z 压缩包(标准版)' } ,
@@ -178,134 +188,40 @@ export default (fastify, options, done) => {
178188 { name : `${ projectName } -green.zip` , desc : 'ZIP 压缩包(绿色版,不含[密]文件)' }
179189 ] ;
180190
181- const html = `
182- <!DOCTYPE html>
183- <html lang="zh-CN">
184- <head>
185- <meta charset="UTF-8">
186- <meta name="viewport" content="width=device-width, initial-scale=1.0">
187- <title>下载 ${ projectName } </title>
188- <style>
189- body {
190- font-family: Arial, sans-serif;
191- max-width: 800px;
192- margin: 50px auto;
193- padding: 20px;
194- background-color: #f5f5f5;
195- }
196- h1 {
197- text-align: center;
198- color: #333;
199- }
200- .download-list {
201- background-color: white;
202- border-radius: 8px;
203- padding: 20px;
204- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
205- }
206- .download-item {
207- margin: 10px 0;
208- padding: 15px;
209- background-color: #f8f9fa;
210- border-radius: 4px;
211- display: flex;
212- justify-content: space-between;
213- align-items: center;
214- }
215- .download-info {
216- flex: 1;
217- }
218- .download-actions {
219- display: flex;
220- gap: 10px;
221- align-items: center;
222- }
223- .download-item a {
224- text-decoration: none;
225- font-weight: bold;
226- padding: 8px 16px;
227- background-color: #007bff;
228- color: white;
229- border-radius: 4px;
230- transition: background-color 0.3s;
231- border: none;
232- cursor: pointer;
233- }
234- .download-item a:hover {
235- background-color: #0056b3;
236- }
237- .copy-btn {
238- text-decoration: none;
239- font-weight: bold;
240- padding: 8px 16px;
241- background-color: #6c757d;
242- color: white;
243- border-radius: 4px;
244- transition: background-color 0.3s;
245- border: none;
246- cursor: pointer;
247- }
248- .copy-btn:hover {
249- background-color: #5a6268;
250- }
251- .file-type {
252- color: #666;
253- font-size: 14px;
254- }
255- .toast {
256- position: fixed;
257- top: 20px;
258- right: 20px;
259- background-color: #28a745;
260- color: white;
261- padding: 10px 20px;
262- border-radius: 4px;
263- display: none;
264- z-index: 1000;
265- }
266- </style>
267- </head>
268- <body>
269- <h1>${ projectName } 下载中心</h1>
270- <div class="toast" id="toast">链接已复制到剪贴板</div>
271- <div class="download-list">
272- ${ files . map ( file => {
191+ const formatFileSize = ( bytes ) => {
192+ if ( ! bytes || bytes === 0 ) return '未打包' ;
193+ const mb = bytes / ( 1024 * 1024 ) ;
194+ return mb . toFixed ( 2 ) + ' MB' ;
195+ } ;
196+
197+ const downloadItems = files . map ( file => {
198+ const latestPackage = findLatestPackage ( projectRootDir , file . name ) ;
199+ const fileSize = latestPackage ? formatFileSize ( latestPackage . size ) : '未打包' ;
200+ const sizeClass = latestPackage ? '' : ' not-packed' ;
273201 const token = generateDownloadToken ( file . name ) ;
274202 const downloadUrl = `/admin/download/${ file . name } ?auth=${ token } ` ;
275- return `
276- <div class="download-item">
277- <div class="download-info">
278- <strong>${ file . name } </strong>
279- <div class="file-type">${ file . desc } </div>
280- </div>
281- <div class="download-actions">
282- <a href="${ downloadUrl } ">下载</a>
283- <button class="copy-btn" onclick="copyLink('${ downloadUrl } ')">复制链接</button>
284- </div>
285- </div>` ;
286- } ) . join ( '' ) }
287- </div>
288- <script>
289- function copyLink(url) {
290- const fullUrl = window.location.origin + url;
291- navigator.clipboard.writeText(fullUrl).then(() => {
292- const toast = document.getElementById('toast');
293- toast.style.display = 'block';
294- setTimeout(() => {
295- toast.style.display = 'none';
296- }, 2000);
297- });
298- }
299- </script>
300- </body>
301- </html>` ;
203+ return '<div class="download-item">' +
204+ '<div class="download-info">' +
205+ '<strong>' + file . name + '</strong>' +
206+ '<div class="file-type">' + file . desc + '</div>' +
207+ '</div>' +
208+ '<div class="download-size' + sizeClass + '">' + fileSize + '</div>' +
209+ '<div class="download-actions">' +
210+ '<a href="' + downloadUrl + '">下载</a>' +
211+ '<button class="copy-btn" onclick="copyLink(\'' + downloadUrl + '\')">复制链接</button>' +
212+ '</div>' +
213+ '</div>' ;
214+ } ) . join ( '' ) ;
215+
216+ html = html . replace ( / \{ \{ p r o j e c t N a m e \} \} / g, projectName ) ;
217+ html = html . replace ( / \{ \{ d o w n l o a d I t e m s \} \} / g, downloadItems ) ;
302218
303219 reply . type ( 'text/html' ) . send ( html ) ;
304220 } catch ( error ) {
305- console . error ( '下载页面加载失败 :' , error . message ) ;
221+ console . error ( '获取下载页面失败 :' , error . message ) ;
306222 return reply . code ( 500 ) . send ( {
307223 success : false ,
308- message : '加载下载页面失败 ' ,
224+ message : '获取下载页面失败 ' ,
309225 error : error . message ,
310226 } ) ;
311227 }
@@ -387,5 +303,53 @@ export default (fastify, options, done) => {
387303 }
388304 } ) ;
389305
306+ fastify . post ( '/admin/download/clear' , {
307+ preHandler : validateBasicAuth
308+ } , async ( request , reply ) => {
309+ try {
310+ if ( IS_VERCEL ) {
311+ return reply . code ( 403 ) . send ( {
312+ success : false ,
313+ message : 'Vercel 环境不支持文件操作' ,
314+ } ) ;
315+ }
316+
317+ const parentDir = path . dirname ( projectRootDir ) ;
318+ const projectName = path . basename ( projectRootDir ) ;
319+ const files = readdirSync ( parentDir ) ;
320+ const pattern = new RegExp ( `^${ projectName . replace ( / \. / g, '\\.' ) } -\\d{8}(-green)?\\.(7z|zip)$` ) ;
321+
322+ let deletedCount = 0 ;
323+ const deletedFiles = [ ] ;
324+
325+ for ( const file of files ) {
326+ if ( pattern . test ( file ) ) {
327+ const filePath = path . join ( parentDir , file ) ;
328+ try {
329+ unlinkSync ( filePath ) ;
330+ deletedFiles . push ( file ) ;
331+ deletedCount ++ ;
332+ } catch ( error ) {
333+ console . error ( `删除文件失败: ${ file } ` , error . message ) ;
334+ }
335+ }
336+ }
337+
338+ return reply . send ( {
339+ success : true ,
340+ count : deletedCount ,
341+ deletedFiles,
342+ message : `已清除 ${ deletedCount } 个历史文件`
343+ } ) ;
344+ } catch ( error ) {
345+ console . error ( '清除历史文件失败:' , error . message ) ;
346+ return reply . code ( 500 ) . send ( {
347+ success : false ,
348+ message : '清除历史文件失败' ,
349+ error : error . message ,
350+ } ) ;
351+ }
352+ } ) ;
353+
390354 done ( ) ;
391355} ;
0 commit comments