update pay ui

This commit is contained in:
2025-12-13 11:38:19 +08:00
parent 646bc25b4f
commit 235cbf48a8
8 changed files with 567 additions and 28 deletions

357
scripts/deploy-to-cos.js Normal file
View File

@@ -0,0 +1,357 @@
#!/usr/bin/env node
/**
* 腾讯云 COS 部署脚本
* 将构建产物上传到 COS 存储桶,配合 CDN 实现静态资源加速
*
* 使用方法:
* node scripts/deploy-to-cos.js
*
* 配置文件:
* .env.cos (从 .env.cos.example 复制并填写)
*/
const COS = require('cos-nodejs-sdk-v5');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// ============================================================================
// 配置加载
// ============================================================================
// 加载 .env.cos 配置
function loadConfig() {
const envPath = path.join(__dirname, '..', '.env.cos');
if (!fs.existsSync(envPath)) {
console.error('\x1b[31m[错误]\x1b[0m 配置文件不存在: .env.cos');
console.log('\n请先创建配置文件:');
console.log(' 1. 复制 .env.cos.example 为 .env.cos');
console.log(' 2. 填写腾讯云 SecretId、SecretKey 等信息\n');
process.exit(1);
}
const envContent = fs.readFileSync(envPath, 'utf-8');
const config = {};
envContent.split('\n').forEach(line => {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#')) {
const [key, ...valueParts] = trimmed.split('=');
if (key) {
config[key.trim()] = valueParts.join('=').trim();
}
}
});
// 验证必需配置
const required = ['COS_SECRET_ID', 'COS_SECRET_KEY', 'COS_BUCKET', 'COS_REGION'];
const missing = required.filter(key => !config[key]);
if (missing.length > 0) {
console.error('\x1b[31m[错误]\x1b[0m 配置不完整,缺少:', missing.join(', '));
process.exit(1);
}
return config;
}
// ============================================================================
// COS 客户端
// ============================================================================
let cos = null;
let config = null;
function initCOS() {
config = loadConfig();
cos = new COS({
SecretId: config.COS_SECRET_ID,
SecretKey: config.COS_SECRET_KEY,
});
console.log('\x1b[32m[✓]\x1b[0m COS 客户端初始化成功');
console.log(` 存储桶: ${config.COS_BUCKET}`);
console.log(` 地域: ${config.COS_REGION}`);
}
// ============================================================================
// 文件操作
// ============================================================================
/**
* 递归获取目录下所有文件
*/
function getAllFiles(dirPath, arrayOfFiles = []) {
const files = fs.readdirSync(dirPath);
files.forEach(file => {
const fullPath = path.join(dirPath, file);
if (fs.statSync(fullPath).isDirectory()) {
getAllFiles(fullPath, arrayOfFiles);
} else {
arrayOfFiles.push(fullPath);
}
});
return arrayOfFiles;
}
/**
* 获取文件的 Content-Type
*/
function getContentType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.webp': 'image/webp',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.map': 'application/json',
'.txt': 'text/plain; charset=utf-8',
'.xml': 'application/xml',
'.webmanifest': 'application/manifest+json',
};
return mimeTypes[ext] || 'application/octet-stream';
}
/**
* 获取缓存控制头
*/
function getCacheControl(filePath) {
const ext = path.extname(filePath).toLowerCase();
const fileName = path.basename(filePath);
// HTML 文件不缓存(或短缓存)
if (ext === '.html') {
return 'no-cache, no-store, must-revalidate';
}
// 带 hash 的静态资源长期缓存JS/CSS 构建产物)
// 文件名格式: main.abc123.js, styles.def456.css
if (/\.[a-f0-9]{8,}\.(js|css)$/.test(fileName)) {
return 'public, max-age=31536000, immutable'; // 1 年
}
// 图片、字体缓存 30 天
if (['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.woff', '.woff2', '.ttf', '.eot'].includes(ext)) {
return 'public, max-age=2592000'; // 30 天
}
// 其他文件缓存 1 天
return 'public, max-age=86400';
}
// ============================================================================
// 上传功能
// ============================================================================
/**
* 上传单个文件到 COS
*/
function uploadFile(localPath, remotePath) {
return new Promise((resolve, reject) => {
const contentType = getContentType(localPath);
const cacheControl = getCacheControl(localPath);
cos.putObject({
Bucket: config.COS_BUCKET,
Region: config.COS_REGION,
Key: remotePath,
Body: fs.createReadStream(localPath),
ContentType: contentType,
CacheControl: cacheControl,
}, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
/**
* 清空存储桶(可选)
*/
async function clearBucket() {
console.log('\n\x1b[36m[清理]\x1b[0m 清空存储桶旧文件...');
return new Promise((resolve, reject) => {
cos.getBucket({
Bucket: config.COS_BUCKET,
Region: config.COS_REGION,
MaxKeys: 1000,
}, async (err, data) => {
if (err) {
reject(err);
return;
}
if (!data.Contents || data.Contents.length === 0) {
console.log(' 存储桶为空,无需清理');
resolve();
return;
}
const objects = data.Contents.map(item => ({ Key: item.Key }));
console.log(` 发现 ${objects.length} 个文件`);
cos.deleteMultipleObject({
Bucket: config.COS_BUCKET,
Region: config.COS_REGION,
Objects: objects,
}, (err, data) => {
if (err) {
reject(err);
} else {
console.log(` 已删除 ${objects.length} 个文件`);
resolve();
}
});
});
});
}
/**
* 上传整个 build 目录
*/
async function uploadBuildDir() {
const buildDir = path.join(__dirname, '..', 'build');
if (!fs.existsSync(buildDir)) {
console.error('\x1b[31m[错误]\x1b[0m build 目录不存在,请先运行 npm run build');
process.exit(1);
}
const files = getAllFiles(buildDir);
console.log(`\n\x1b[36m[上传]\x1b[0m 共 ${files.length} 个文件待上传...\n`);
let uploaded = 0;
let failed = 0;
const errors = [];
// 并发上传(限制并发数)
const concurrency = 10;
const chunks = [];
for (let i = 0; i < files.length; i += concurrency) {
chunks.push(files.slice(i, i + concurrency));
}
for (const chunk of chunks) {
await Promise.all(chunk.map(async (filePath) => {
const relativePath = path.relative(buildDir, filePath).replace(/\\/g, '/');
try {
await uploadFile(filePath, relativePath);
uploaded++;
// 进度显示
const progress = Math.round((uploaded + failed) / files.length * 100);
process.stdout.write(`\r 进度: ${progress}% (${uploaded}/${files.length})`);
} catch (err) {
failed++;
errors.push({ file: relativePath, error: err.message });
}
}));
}
console.log('\n');
if (failed > 0) {
console.log('\x1b[33m[警告]\x1b[0m 部分文件上传失败:');
errors.forEach(({ file, error }) => {
console.log(` - ${file}: ${error}`);
});
}
console.log(`\x1b[32m[✓]\x1b[0m 上传完成: 成功 ${uploaded},失败 ${failed}`);
return { uploaded, failed };
}
// ============================================================================
// CDN 刷新(可选)
// ============================================================================
/**
* 刷新 CDN 缓存
* 注意:需要额外配置 CDN API 权限
*/
async function refreshCDN() {
if (!config.CDN_DOMAIN) {
console.log('\n\x1b[33m[提示]\x1b[0m 未配置 CDN 域名,跳过 CDN 刷新');
console.log(' 如需自动刷新 CDN请在 .env.cos 中配置 CDN_DOMAIN');
return;
}
console.log('\n\x1b[36m[CDN]\x1b[0m 刷新 CDN 缓存...');
console.log(` 域名: ${config.CDN_DOMAIN}`);
console.log('\n\x1b[33m[提示]\x1b[0m 由于文件名包含 hash通常不需要手动刷新 CDN');
console.log(' 如需刷新,请到腾讯云 CDN 控制台操作:');
console.log(' https://console.cloud.tencent.com/cdn/refresh');
}
// ============================================================================
// 主流程
// ============================================================================
async function main() {
console.log('\n╔════════════════════════════════════════════════════════════════╗');
console.log('║ 腾讯云 COS 部署工具 - ValueFrontier ║');
console.log('╚════════════════════════════════════════════════════════════════╝\n');
const startTime = Date.now();
try {
// 1. 初始化 COS 客户端
initCOS();
// 2. 可选:清空存储桶
const clearFirst = process.argv.includes('--clear');
if (clearFirst) {
await clearBucket();
}
// 3. 上传文件
const { uploaded, failed } = await uploadBuildDir();
// 4. CDN 刷新提示
await refreshCDN();
// 5. 完成
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
console.log('\n╔════════════════════════════════════════════════════════════════╗');
console.log('║ 🎉 部署成功! ║');
console.log('╚════════════════════════════════════════════════════════════════╝');
console.log(`\n 耗时: ${duration}`);
console.log(` 上传: ${uploaded} 个文件`);
if (config.CDN_DOMAIN) {
console.log(`\n 访问地址: https://${config.CDN_DOMAIN}`);
}
console.log('\n');
} catch (err) {
console.error('\n\x1b[31m[错误]\x1b[0m 部署失败:', err.message);
process.exit(1);
}
}
// 运行
main();