From a9082cc463bcea395accf80134b26769bfdd973e Mon Sep 17 00:00:00 2001 From: zzlgreat Date: Fri, 9 Jan 2026 11:10:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E9=83=A8=E7=BD=B2js?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + scripts/deploy-to-cos.js | 90 +++++++++++++-- .../Community/components/ThemeCometChart.js | 109 ++++++++++++------ 3 files changed, 159 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index e0601b91..6391ce2a 100755 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "prettier": "2.2.1", "react-error-overlay": "6.0.9", "sharp": "^0.34.4", + "tencentcloud-sdk-nodejs-cdn": "^4.1.161", "ts-node": "^10.9.2", "webpack-bundle-analyzer": "^4.10.2", "yn": "^5.1.0" diff --git a/scripts/deploy-to-cos.js b/scripts/deploy-to-cos.js index ee5a0952..0af576d5 100644 --- a/scripts/deploy-to-cos.js +++ b/scripts/deploy-to-cos.js @@ -15,6 +15,15 @@ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); +// CDN SDK (可选,如果安装了的话) +let CdnClient = null; +try { + const tencentcloud = require('tencentcloud-sdk-nodejs-cdn'); + CdnClient = tencentcloud.cdn.v20180606.Client; +} catch (e) { + // CDN SDK 未安装,稍后会提示用户 +} + // ============================================================================ // 配置加载 // ============================================================================ @@ -284,25 +293,86 @@ async function uploadBuildDir() { } // ============================================================================ -// CDN 刷新(可选) +// CDN 刷新 // ============================================================================ /** * 刷新 CDN 缓存 - * 注意:需要额外配置 CDN API 权限 + * 自动刷新 index.html 等关键文件,确保用户获取最新版本 */ async function refreshCDN() { if (!config.CDN_DOMAIN) { console.log('\n\x1b[33m[提示]\x1b[0m 未配置 CDN 域名,跳过 CDN 刷新'); console.log(' 如需自动刷新 CDN,请在 .env.cos 中配置 CDN_DOMAIN'); - return; + return { success: false, reason: 'no_domain' }; + } + + // 检查是否安装了 CDN SDK + if (!CdnClient) { + console.log('\n\x1b[33m[提示]\x1b[0m CDN SDK 未安装,跳过自动刷新'); + console.log(' 安装命令: npm install tencentcloud-sdk-nodejs-cdn --save-dev'); + console.log(' 或手动刷新: https://console.cloud.tencent.com/cdn/refresh'); + return { success: false, reason: 'no_sdk' }; } 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'); + + try { + // 初始化 CDN 客户端 + const cdnClient = new CdnClient({ + credential: { + secretId: config.COS_SECRET_ID, + secretKey: config.COS_SECRET_KEY, + }, + region: '', // CDN 为全局服务,region 留空 + profile: { + signMethod: 'TC3-HMAC-SHA256', + httpProfile: { + reqMethod: 'POST', + reqTimeout: 30, + }, + }, + }); + + // 需要刷新的关键文件(不带 hash 的文件) + const domain = config.CDN_DOMAIN.replace(/^https?:\/\//, ''); + const urlsToRefresh = [ + `https://${domain}/index.html`, + `https://${domain}/`, + `https://${domain}/asset-manifest.json`, + `https://${domain}/manifest.json`, + ]; + + console.log(' 刷新文件:'); + urlsToRefresh.forEach(url => console.log(` - ${url}`)); + + // 调用 PurgeUrlsCache 接口 + const result = await cdnClient.PurgeUrlsCache({ + Urls: urlsToRefresh, + }); + + console.log(`\n\x1b[32m[✓]\x1b[0m CDN 刷新任务已提交`); + console.log(` 任务ID: ${result.TaskId}`); + console.log(' 刷新生效时间约 5 分钟'); + + return { success: true, taskId: result.TaskId }; + + } catch (err) { + console.error(`\n\x1b[33m[警告]\x1b[0m CDN 刷新失败: ${err.message}`); + + // 提供常见错误的解决方案 + if (err.code === 'AuthFailure.SecretIdNotFound' || err.code === 'AuthFailure.SignatureFailure') { + console.log(' 请检查 API 密钥是否正确'); + } else if (err.code === 'UnauthorizedOperation') { + console.log(' 请确保 API 密钥有 CDN 刷新权限'); + console.log(' 权限策略: QcloudCDNFullAccess 或自定义 cdn:PurgeUrlsCache'); + } + + console.log('\n 可手动刷新: https://console.cloud.tencent.com/cdn/refresh'); + + return { success: false, reason: 'api_error', error: err.message }; + } } // ============================================================================ @@ -329,8 +399,8 @@ async function main() { // 3. 上传文件 const { uploaded, failed } = await uploadBuildDir(); - // 4. CDN 刷新提示 - await refreshCDN(); + // 4. CDN 刷新 + const cdnResult = await refreshCDN(); // 5. 完成 const duration = ((Date.now() - startTime) / 1000).toFixed(1); @@ -340,9 +410,13 @@ async function main() { console.log('╚════════════════════════════════════════════════════════════════╝'); console.log(`\n 耗时: ${duration} 秒`); console.log(` 上传: ${uploaded} 个文件`); + console.log(` CDN刷新: ${cdnResult.success ? '✓ 已提交' : '✗ 跳过'}`); if (config.CDN_DOMAIN) { console.log(`\n 访问地址: https://${config.CDN_DOMAIN}`); + if (cdnResult.success) { + console.log(' 提示: CDN 刷新约需 5 分钟生效'); + } } console.log('\n'); diff --git a/src/views/Community/components/ThemeCometChart.js b/src/views/Community/components/ThemeCometChart.js index 4f82481f..6e4f3acf 100644 --- a/src/views/Community/components/ThemeCometChart.js +++ b/src/views/Community/components/ThemeCometChart.js @@ -195,30 +195,68 @@ const generateChartOption = (themes) => { */ const ThemeCometChart = ({ onThemeSelect }) => { const [loading, setLoading] = useState(true); - const [data, setData] = useState({ themes: [], currentDate: '', availableDates: [] }); + const [allDatesData, setAllDatesData] = useState({}); // 缓存所有日期的数据 + const [availableDates, setAvailableDates] = useState([]); const [selectedTheme, setSelectedTheme] = useState(null); const [sliderIndex, setSliderIndex] = useState(0); const [showTooltip, setShowTooltip] = useState(false); const { isOpen, onOpen, onClose } = useDisclosure(); const toast = useToast(); - // 加载指定日期的数据 - const loadData = useCallback(async (dateStr = '') => { + // 加载所有日期的数据 + const loadAllData = useCallback(async () => { setLoading(true); try { const apiBase = getApiBase(); - const url = dateStr - ? `${apiBase}/api/v1/zt/theme-scatter?date=${dateStr}&days=5` - : `${apiBase}/api/v1/zt/theme-scatter?days=5`; - const response = await fetch(url); + // 先获取最新数据,拿到可用日期列表 + const response = await fetch(`${apiBase}/api/v1/zt/theme-scatter?days=5`); const result = await response.json(); if (result.success && result.data) { - setData({ - themes: result.data.themes || [], - currentDate: result.data.currentDate || '', - availableDates: result.data.availableDates || [], + const dates = result.data.availableDates || []; + setAvailableDates(dates); + + // 缓存第一个日期(最新)的数据 + const latestDate = dates[0]?.date; + const dataCache = {}; + if (latestDate) { + dataCache[latestDate] = { + themes: result.data.themes || [], + currentDate: result.data.currentDate || '', + }; + } + + // 并行加载其他日期的数据 + const otherDates = dates.slice(1); + const promises = otherDates.map(async (dateInfo) => { + try { + const res = await fetch(`${apiBase}/api/v1/zt/theme-scatter?date=${dateInfo.date}&days=5`); + const data = await res.json(); + if (data.success && data.data) { + return { + date: dateInfo.date, + themes: data.data.themes || [], + currentDate: data.data.currentDate || '', + }; + } + } catch (e) { + console.error(`加载 ${dateInfo.date} 数据失败:`, e); + } + return null; }); + + const results = await Promise.all(promises); + results.forEach((item) => { + if (item) { + dataCache[item.date] = { + themes: item.themes, + currentDate: item.currentDate, + }; + } + }); + + setAllDatesData(dataCache); + setSliderIndex(0); // 默认显示最新日期 } else { throw new Error(result.error || '加载失败'); } @@ -230,43 +268,42 @@ const ThemeCometChart = ({ onThemeSelect }) => { } }, [toast]); - // 初始加载 + // 初始加载所有数据 useEffect(() => { - loadData(); - }, [loadData]); + loadAllData(); + }, [loadAllData]); - // 滑动条变化时加载对应日期数据 + // 滑动条变化时实时切换数据 const handleSliderChange = useCallback((value) => { setSliderIndex(value); }, []); - const handleSliderChangeEnd = useCallback((value) => { - if (data.availableDates && data.availableDates[value]) { - loadData(data.availableDates[value].date); - } - }, [data.availableDates, loadData]); + // 获取当前显示的数据 + const currentDateStr = availableDates[sliderIndex]?.date; + const currentData = allDatesData[currentDateStr] || { themes: [], currentDate: '' }; + const isCurrentDateLoaded = currentDateStr && allDatesData[currentDateStr]; - const chartOption = useMemo(() => generateChartOption(data.themes), [data.themes]); + const chartOption = useMemo(() => generateChartOption(currentData.themes), [currentData.themes]); const handleChartClick = useCallback( (params) => { if (params.data) { - const theme = data.themes.find((t) => t.label === params.data.name); + const theme = currentData.themes.find((t) => t.label === params.data.name); if (theme) { setSelectedTheme(theme); onOpen(); } } }, - [data.themes, onOpen] + [currentData.themes, onOpen] ); const onChartEvents = useMemo(() => ({ click: handleChartClick }), [handleChartClick]); // 当前滑动条对应的日期 - const currentSliderDate = data.availableDates?.[sliderIndex]?.formatted || data.currentDate; + const currentSliderDate = availableDates[sliderIndex]?.formatted || currentData.currentDate; - if (loading && data.themes.length === 0) { + if (loading && Object.keys(allDatesData).length === 0) { return (
@@ -299,14 +336,21 @@ const ThemeCometChart = ({ onThemeSelect }) => { {loading && } - {data.currentDate} + {currentSliderDate || currentData.currentDate} {/* 图表区域 */} - - {data.themes.length > 0 ? ( + + {!isCurrentDateLoaded && !loading ? ( +
+ + + 加载中... + +
+ ) : currentData.themes.length > 0 ? ( {
{/* 时间滑动条 */} - {data.availableDates.length > 1 && ( + {availableDates.length > 1 && ( - {data.availableDates[data.availableDates.length - 1]?.formatted?.slice(5) || ''} + {availableDates[availableDates.length - 1]?.formatted?.slice(5) || ''} { setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} isReversed @@ -360,7 +403,7 @@ const ThemeCometChart = ({ onThemeSelect }) => { - {data.availableDates[0]?.formatted?.slice(5) || ''} + {availableDates[0]?.formatted?.slice(5) || ''}