From 9addbe190e1bdcbc3c96eee9b187b329cdb0ba04 Mon Sep 17 00:00:00 2001 From: ltx <1006158920@qq.com> Date: Tue, 14 Jul 2026 10:05:12 +0800 Subject: [PATCH] =?UTF-8?q?update=EF=BC=9A=E8=87=AA=E5=8A=A8=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=89=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- .gitignore | 1 + .lingma/rules/AAA.md | 4 - package.json | 5 +- scripts/build-and-upload-oss.cjs | 154 +++++++++++++++++++++++++++++++ vite.config.ts | 12 ++- 6 files changed, 169 insertions(+), 9 deletions(-) delete mode 100644 .lingma/rules/AAA.md create mode 100644 scripts/build-and-upload-oss.cjs diff --git a/.env b/.env index c9c73f6..8d863ea 100644 --- a/.env +++ b/.env @@ -8,4 +8,4 @@ VITE_OPEN = true VITE_OPEN_CDN = false # public path 配置线上环境路径(打包)、本地通过 http-server 访问时,请置空即可 -VITE_PUBLIC_PATH = 'https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_web/202606301530/' \ No newline at end of file +VITE_PUBLIC_PATH = 'https://yuedong-wechatapplet.oss-cn-shanghai.aliyuncs.com/static/event_web/202607140930/' \ No newline at end of file diff --git a/.gitignore b/.gitignore index 403adbc..277aa9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store node_modules /dist +/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/ # local env files diff --git a/.lingma/rules/AAA.md b/.lingma/rules/AAA.md deleted file mode 100644 index e96892e..0000000 --- a/.lingma/rules/AAA.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -trigger: always_on ---- - diff --git a/package.json b/package.json index 4062977..3deee1e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "license": "MIT", "scripts": { "dev": "vite", - "build": "vite build", + "build": "node scripts/build-and-upload-oss.cjs", + "build:local": "vite build --base ./", "lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/", "gen": "sta -p http://localhost:53785/swagger/Default/swagger.json -o ./src/apis --modular --route-types --axios --module-name-index 1 --extract-request-params -t ./templates" }, @@ -91,4 +92,4 @@ "type": "git", "url": "https://gitee.com/lyt-top/vue-next-admin.git" } -} \ No newline at end of file +} diff --git a/scripts/build-and-upload-oss.cjs b/scripts/build-and-upload-oss.cjs new file mode 100644 index 0000000..172cafc --- /dev/null +++ b/scripts/build-and-upload-oss.cjs @@ -0,0 +1,154 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const https = require('https'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const bucket = 'yuedong-wechatapplet'; +const endpoint = 'oss-cn-shanghai.aliyuncs.com'; +const uploadPrefix = 'static/event_web'; +const publicRoot = `https://${bucket}.${endpoint}/${uploadPrefix}`; +const concurrency = 8; + +const pad = (value) => value.toString().padStart(2, '0'); +const now = new Date(); +const buildDirectory = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad( + now.getMinutes() +)}`; +const outputPath = path.resolve(process.cwd(), buildDirectory); +const publicPath = `${publicRoot}/${buildDirectory}/`; +const dryRun = process.env.OSS_DRY_RUN === '1'; + +const accessKeyId = 'LTAI5t79WkXht8yy8aAAZ5qz'; +const accessKeySecret = 'RpJ5miIeGARvfy2G1P8HtWvdR4j6rq'; +const securityToken = process.env.ALIYUN_OSS_SECURITY_TOKEN || process.env.OSS_SECURITY_TOKEN; + +if (!dryRun && (!accessKeyId || !accessKeySecret)) { + console.error('缺少阿里云 OSS 凭证,请设置 ALIYUN_OSS_ACCESS_KEY_ID 和 ALIYUN_OSS_ACCESS_KEY_SECRET。'); + process.exit(1); +} + +console.log(`开始打包:${buildDirectory}`); +console.log(`资源地址:${publicPath}`); + +const viteCli = path.resolve(process.cwd(), 'node_modules/vite/bin/vite.js'); +const buildEnvironment = { ...process.env }; +[ + 'ALIYUN_OSS_ACCESS_KEY_ID', + 'ALIYUN_OSS_ACCESS_KEY_SECRET', + 'ALIYUN_OSS_SECURITY_TOKEN', + 'OSS_ACCESS_KEY_ID', + 'OSS_ACCESS_KEY_SECRET', + 'OSS_SECURITY_TOKEN', +].forEach((key) => delete buildEnvironment[key]); + +const buildResult = spawnSync(process.execPath, [viteCli, 'build'], { + stdio: 'inherit', + env: { + ...buildEnvironment, + BUILD_OUT_DIR: buildDirectory, + BUILD_PUBLIC_PATH: publicPath, + }, +}); + +if (buildResult.error) throw buildResult.error; +if (buildResult.status !== 0) process.exit(buildResult.status || 1); + +const getFiles = (directory) => + fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const fullPath = path.join(directory, entry.name); + return entry.isDirectory() ? getFiles(fullPath) : [fullPath]; + }); + +const contentTypes = { + '.css': 'text/css; charset=utf-8', + '.eot': 'application/vnd.ms-fontobject', + '.gif': 'image/gif', + '.html': 'text/html; charset=utf-8', + '.ico': 'image/x-icon', + '.jpeg': 'image/jpeg', + '.jpg': 'image/jpeg', + '.js': 'application/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.map': 'application/json; charset=utf-8', + '.mp3': 'audio/mpeg', + '.mp4': 'video/mp4', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.ttf': 'font/ttf', + '.webp': 'image/webp', + '.woff': 'font/woff', + '.woff2': 'font/woff2', +}; + +const encodeObjectKey = (objectKey) => objectKey.split('/').map(encodeURIComponent).join('/'); + +const uploadFile = (filePath, objectKey, attempt = 1) => + new Promise((resolve, reject) => { + const date = new Date().toUTCString(); + const contentType = contentTypes[path.extname(filePath).toLowerCase()] || 'application/octet-stream'; + const canonicalHeaders = securityToken ? `x-oss-security-token:${securityToken}\n` : ''; + const canonicalResource = `/${bucket}/${objectKey}`; + const stringToSign = `PUT\n\n${contentType}\n${date}\n${canonicalHeaders}${canonicalResource}`; + const signature = crypto.createHmac('sha1', accessKeySecret).update(stringToSign).digest('base64'); + const headers = { + Authorization: `OSS ${accessKeyId}:${signature}`, + 'Content-Length': fs.statSync(filePath).size, + 'Content-Type': contentType, + Date: date, + }; + if (securityToken) headers['x-oss-security-token'] = securityToken; + + const request = https.request( + { + host: `${bucket}.${endpoint}`, + path: `/${encodeObjectKey(objectKey)}`, + method: 'PUT', + headers, + }, + (response) => { + let responseBody = ''; + response.setEncoding('utf8'); + response.on('data', (chunk) => (responseBody += chunk)); + response.on('end', () => { + if (response.statusCode >= 200 && response.statusCode < 300) return resolve(); + const error = new Error(`OSS 上传失败 (${response.statusCode}):${objectKey}\n${responseBody.slice(0, 2000)}`); + if (response.statusCode >= 500 && attempt < 3) return setTimeout(() => resolve(uploadFile(filePath, objectKey, attempt + 1)), 500 * attempt); + reject(error); + }); + } + ); + + request.on('error', (error) => { + if (attempt < 3) return setTimeout(() => resolve(uploadFile(filePath, objectKey, attempt + 1)), 500 * attempt); + reject(error); + }); + fs.createReadStream(filePath).pipe(request); + }); + +const files = getFiles(outputPath).sort(); + +if (dryRun) { + console.log(`[DRY RUN] 已生成 ${files.length} 个文件,将上传到 ${publicPath}`); + process.exit(0); +} + +let nextIndex = 0; +let uploaded = 0; +const worker = async () => { + while (nextIndex < files.length) { + const filePath = files[nextIndex++]; + const relativePath = path.relative(outputPath, filePath).split(path.sep).join('/'); + const objectKey = `${uploadPrefix}/${buildDirectory}/${relativePath}`; + await uploadFile(filePath, objectKey); + uploaded += 1; + if (uploaded % 50 === 0 || uploaded === files.length) console.log(`上传进度:${uploaded}/${files.length}`); + } +}; + +Promise.all(Array.from({ length: Math.min(concurrency, files.length) }, worker)) + .then(() => console.log(`部署完成:${publicPath}`)) + .catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); diff --git a/vite.config.ts b/vite.config.ts index da1de85..db9d8c2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -15,13 +15,21 @@ const alias: Record = { 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js', }; +const getBuildTimeDirectory = () => { + const now = new Date(); + const pad = (value: number) => value.toString().padStart(2, '0'); + + return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}`; +}; + const viteConfig = defineConfig((mode: ConfigEnv) => { const env = loadEnv(mode.mode, process.cwd()); + const outDir = mode.command === 'build' ? process.env.BUILD_OUT_DIR || getBuildTimeDirectory() : 'dist'; return { plugins: [vue(), vueJsx(), vueSetupExtend(), viteCompression(), JSON.parse(env.VITE_OPEN_CDN) ? buildConfig.cdn() : null], root: process.cwd(), resolve: { alias }, - base: mode.command === 'serve' ? './' : env.VITE_PUBLIC_PATH, + base: mode.command === 'serve' ? './' : process.env.BUILD_PUBLIC_PATH || env.VITE_PUBLIC_PATH, optimizeDeps: { exclude: ['vue-demi'] }, server: { host: '0.0.0.0', @@ -39,7 +47,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => { }, }, build: { - outDir: 'dist', + outDir, chunkSizeWarningLimit: 1500, rollupOptions: { output: {