修改部署js
This commit is contained in:
@@ -117,6 +117,7 @@
|
|||||||
"prettier": "2.2.1",
|
"prettier": "2.2.1",
|
||||||
"react-error-overlay": "6.0.9",
|
"react-error-overlay": "6.0.9",
|
||||||
"sharp": "^0.34.4",
|
"sharp": "^0.34.4",
|
||||||
|
"tencentcloud-sdk-nodejs-cdn": "^4.1.161",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"webpack-bundle-analyzer": "^4.10.2",
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
"yn": "^5.1.0"
|
"yn": "^5.1.0"
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { execSync } = require('child_process');
|
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 缓存
|
||||||
* 注意:需要额外配置 CDN API 权限
|
* 自动刷新 index.html 等关键文件,确保用户获取最新版本
|
||||||
*/
|
*/
|
||||||
async function refreshCDN() {
|
async function refreshCDN() {
|
||||||
if (!config.CDN_DOMAIN) {
|
if (!config.CDN_DOMAIN) {
|
||||||
console.log('\n\x1b[33m[提示]\x1b[0m 未配置 CDN 域名,跳过 CDN 刷新');
|
console.log('\n\x1b[33m[提示]\x1b[0m 未配置 CDN 域名,跳过 CDN 刷新');
|
||||||
console.log(' 如需自动刷新 CDN,请在 .env.cos 中配置 CDN_DOMAIN');
|
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('\n\x1b[36m[CDN]\x1b[0m 刷新 CDN 缓存...');
|
||||||
console.log(` 域名: ${config.CDN_DOMAIN}`);
|
console.log(` 域名: ${config.CDN_DOMAIN}`);
|
||||||
console.log('\n\x1b[33m[提示]\x1b[0m 由于文件名包含 hash,通常不需要手动刷新 CDN');
|
|
||||||
console.log(' 如需刷新,请到腾讯云 CDN 控制台操作:');
|
try {
|
||||||
console.log(' https://console.cloud.tencent.com/cdn/refresh');
|
// 初始化 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. 上传文件
|
// 3. 上传文件
|
||||||
const { uploaded, failed } = await uploadBuildDir();
|
const { uploaded, failed } = await uploadBuildDir();
|
||||||
|
|
||||||
// 4. CDN 刷新提示
|
// 4. CDN 刷新
|
||||||
await refreshCDN();
|
const cdnResult = await refreshCDN();
|
||||||
|
|
||||||
// 5. 完成
|
// 5. 完成
|
||||||
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||||
@@ -340,9 +410,13 @@ async function main() {
|
|||||||
console.log('╚════════════════════════════════════════════════════════════════╝');
|
console.log('╚════════════════════════════════════════════════════════════════╝');
|
||||||
console.log(`\n 耗时: ${duration} 秒`);
|
console.log(`\n 耗时: ${duration} 秒`);
|
||||||
console.log(` 上传: ${uploaded} 个文件`);
|
console.log(` 上传: ${uploaded} 个文件`);
|
||||||
|
console.log(` CDN刷新: ${cdnResult.success ? '✓ 已提交' : '✗ 跳过'}`);
|
||||||
|
|
||||||
if (config.CDN_DOMAIN) {
|
if (config.CDN_DOMAIN) {
|
||||||
console.log(`\n 访问地址: https://${config.CDN_DOMAIN}`);
|
console.log(`\n 访问地址: https://${config.CDN_DOMAIN}`);
|
||||||
|
if (cdnResult.success) {
|
||||||
|
console.log(' 提示: CDN 刷新约需 5 分钟生效');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n');
|
console.log('\n');
|
||||||
|
|||||||
@@ -195,30 +195,68 @@ const generateChartOption = (themes) => {
|
|||||||
*/
|
*/
|
||||||
const ThemeCometChart = ({ onThemeSelect }) => {
|
const ThemeCometChart = ({ onThemeSelect }) => {
|
||||||
const [loading, setLoading] = useState(true);
|
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 [selectedTheme, setSelectedTheme] = useState(null);
|
||||||
const [sliderIndex, setSliderIndex] = useState(0);
|
const [sliderIndex, setSliderIndex] = useState(0);
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
// 加载指定日期的数据
|
// 加载所有日期的数据
|
||||||
const loadData = useCallback(async (dateStr = '') => {
|
const loadAllData = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const apiBase = getApiBase();
|
const apiBase = getApiBase();
|
||||||
const url = dateStr
|
// 先获取最新数据,拿到可用日期列表
|
||||||
? `${apiBase}/api/v1/zt/theme-scatter?date=${dateStr}&days=5`
|
const response = await fetch(`${apiBase}/api/v1/zt/theme-scatter?days=5`);
|
||||||
: `${apiBase}/api/v1/zt/theme-scatter?days=5`;
|
|
||||||
const response = await fetch(url);
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
||||||
if (result.success && result.data) {
|
if (result.success && result.data) {
|
||||||
setData({
|
const dates = result.data.availableDates || [];
|
||||||
themes: result.data.themes || [],
|
setAvailableDates(dates);
|
||||||
currentDate: result.data.currentDate || '',
|
|
||||||
availableDates: result.data.availableDates || [],
|
// 缓存第一个日期(最新)的数据
|
||||||
|
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 {
|
} else {
|
||||||
throw new Error(result.error || '加载失败');
|
throw new Error(result.error || '加载失败');
|
||||||
}
|
}
|
||||||
@@ -230,43 +268,42 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
}
|
}
|
||||||
}, [toast]);
|
}, [toast]);
|
||||||
|
|
||||||
// 初始加载
|
// 初始加载所有数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
loadAllData();
|
||||||
}, [loadData]);
|
}, [loadAllData]);
|
||||||
|
|
||||||
// 滑动条变化时加载对应日期数据
|
// 滑动条变化时实时切换数据
|
||||||
const handleSliderChange = useCallback((value) => {
|
const handleSliderChange = useCallback((value) => {
|
||||||
setSliderIndex(value);
|
setSliderIndex(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSliderChangeEnd = useCallback((value) => {
|
// 获取当前显示的数据
|
||||||
if (data.availableDates && data.availableDates[value]) {
|
const currentDateStr = availableDates[sliderIndex]?.date;
|
||||||
loadData(data.availableDates[value].date);
|
const currentData = allDatesData[currentDateStr] || { themes: [], currentDate: '' };
|
||||||
}
|
const isCurrentDateLoaded = currentDateStr && allDatesData[currentDateStr];
|
||||||
}, [data.availableDates, loadData]);
|
|
||||||
|
|
||||||
const chartOption = useMemo(() => generateChartOption(data.themes), [data.themes]);
|
const chartOption = useMemo(() => generateChartOption(currentData.themes), [currentData.themes]);
|
||||||
|
|
||||||
const handleChartClick = useCallback(
|
const handleChartClick = useCallback(
|
||||||
(params) => {
|
(params) => {
|
||||||
if (params.data) {
|
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) {
|
if (theme) {
|
||||||
setSelectedTheme(theme);
|
setSelectedTheme(theme);
|
||||||
onOpen();
|
onOpen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[data.themes, onOpen]
|
[currentData.themes, onOpen]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChartEvents = useMemo(() => ({ click: handleChartClick }), [handleChartClick]);
|
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 (
|
return (
|
||||||
<Center h="300px">
|
<Center h="300px">
|
||||||
<VStack spacing={4}>
|
<VStack spacing={4}>
|
||||||
@@ -299,14 +336,21 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
{loading && <Spinner size="sm" color="yellow.400" />}
|
{loading && <Spinner size="sm" color="yellow.400" />}
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text fontSize="sm" color="whiteAlpha.500">
|
<Text fontSize="sm" color="whiteAlpha.500">
|
||||||
{data.currentDate}
|
{currentSliderDate || currentData.currentDate}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* 图表区域 */}
|
{/* 图表区域 */}
|
||||||
<Box h="calc(100% - 100px)">
|
<Box h="calc(100% - 100px)" position="relative">
|
||||||
{data.themes.length > 0 ? (
|
{!isCurrentDateLoaded && !loading ? (
|
||||||
|
<Center h="100%">
|
||||||
|
<VStack spacing={2}>
|
||||||
|
<Spinner size="md" color="yellow.400" />
|
||||||
|
<Text color="whiteAlpha.500" fontSize="sm">加载中...</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
) : currentData.themes.length > 0 ? (
|
||||||
<ReactECharts
|
<ReactECharts
|
||||||
option={chartOption}
|
option={chartOption}
|
||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
@@ -321,11 +365,11 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* 时间滑动条 */}
|
{/* 时间滑动条 */}
|
||||||
{data.availableDates.length > 1 && (
|
{availableDates.length > 1 && (
|
||||||
<Box px={2} pt={2}>
|
<Box px={2} pt={2}>
|
||||||
<HStack spacing={3}>
|
<HStack spacing={3}>
|
||||||
<Text fontSize="sm" color="whiteAlpha.500" whiteSpace="nowrap">
|
<Text fontSize="sm" color="whiteAlpha.500" whiteSpace="nowrap">
|
||||||
{data.availableDates[data.availableDates.length - 1]?.formatted?.slice(5) || ''}
|
{availableDates[availableDates.length - 1]?.formatted?.slice(5) || ''}
|
||||||
</Text>
|
</Text>
|
||||||
<ChakraTooltip
|
<ChakraTooltip
|
||||||
hasArrow
|
hasArrow
|
||||||
@@ -338,11 +382,10 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
<Slider
|
<Slider
|
||||||
aria-label="date-slider"
|
aria-label="date-slider"
|
||||||
min={0}
|
min={0}
|
||||||
max={data.availableDates.length - 1}
|
max={availableDates.length - 1}
|
||||||
step={1}
|
step={1}
|
||||||
value={sliderIndex}
|
value={sliderIndex}
|
||||||
onChange={handleSliderChange}
|
onChange={handleSliderChange}
|
||||||
onChangeEnd={handleSliderChangeEnd}
|
|
||||||
onMouseEnter={() => setShowTooltip(true)}
|
onMouseEnter={() => setShowTooltip(true)}
|
||||||
onMouseLeave={() => setShowTooltip(false)}
|
onMouseLeave={() => setShowTooltip(false)}
|
||||||
isReversed
|
isReversed
|
||||||
@@ -360,7 +403,7 @@ const ThemeCometChart = ({ onThemeSelect }) => {
|
|||||||
</Slider>
|
</Slider>
|
||||||
</ChakraTooltip>
|
</ChakraTooltip>
|
||||||
<Text fontSize="sm" color="whiteAlpha.500" whiteSpace="nowrap">
|
<Text fontSize="sm" color="whiteAlpha.500" whiteSpace="nowrap">
|
||||||
{data.availableDates[0]?.formatted?.slice(5) || ''}
|
{availableDates[0]?.formatted?.slice(5) || ''}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user