diff --git a/bun.lockb b/bun.lockb index 7f28d98..82e9e99 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 18f72b2..c8135a4 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "file-saver": "^2.0.5", "grapheme-splitter": "^1.0.4", "html2canvas": "^1.4.1", + "jszip": "^3.10.1", "idb-keyval": "^6.2.2", "linqts": "^2.0.0", "lodash": "^4.17.21", @@ -81,6 +82,7 @@ "devDependencies": { "@types/bun": "^1.2.16", "@types/file-saver": "^2.0.7", + "@types/jszip": "^3.10.6", "@types/uuid": "^10.0.0", "@vicons/ionicons5": "^0.13.0", "@vitejs/plugin-vue-jsx": "^4.2.0", diff --git a/src/api/api-models.ts b/src/api/api-models.ts index 0465d48..17da721 100644 --- a/src/api/api-models.ts +++ b/src/api/api-models.ts @@ -143,6 +143,11 @@ export interface Setting_Index { videos: string[] notification: string links: { [key: string]: string } + /** + * 自定义链接顺序(存储链接名称的有序数组) + * 旧数据无此字段时在前端初始化为 Object.keys(links) + */ + linkOrder?: string[] } export interface Setting_LiveRequest { orderPrefix: string @@ -824,6 +829,7 @@ export interface ResponseUserIndexModel { notification: string videos: VideoCollectVideo[] links: { [key: string]: string } + linkOrder?: string[] } // 签到排行信息 diff --git a/src/components.d.ts b/src/components.d.ts index ff637fb..32b6e3c 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -31,6 +31,7 @@ declare module 'vue' { NIcon: typeof import('naive-ui')['NIcon'] NImage: typeof import('naive-ui')['NImage'] NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel'] + NInputNumber: typeof import('naive-ui')['NInputNumber'] NModal: typeof import('naive-ui')['NModal'] NPopconfirm: typeof import('naive-ui')['NPopconfirm'] NScrollbar: typeof import('naive-ui')['NScrollbar'] diff --git a/src/components/manage/tools/ToolDynamicNineGrid.vue b/src/components/manage/tools/ToolDynamicNineGrid.vue deleted file mode 100644 index 53046a4..0000000 --- a/src/components/manage/tools/ToolDynamicNineGrid.vue +++ /dev/null @@ -1,608 +0,0 @@ - - - - - - - 上传原始图片 - - - 请上传一张方形或长方形图片,将会自动分割成3x3的九宫格图片 - - - - 请确保您上传的图片是方形或近似方形,以获得最佳效果 - - - - - 生成预览 - - 重新上传 - - - - 九宫格预览 - - 九宫格预览显示了图片分割后的效果,每个格子都可以添加下方图片 - - - - - - - {{ i }} - - - - - - - - 第{{ i }}格 - - - - - - - - - handleAdditionalImageChange(data, i - 1)" - accept="image/*" - > - 添加下方图片 - - removeAdditionalImage(i-1)" - > - 删除下方图片 - - - - - - 生成九张图片 - - 打包下载全部 - - - - 最终图片 - - 以下是生成的九宫格图片,您可以单独下载每张图片 - - - - - {{ index + 1 }} - downloadImage(imgDataUrl, `grid_image_${index + 1}.png`)" class="download-button"> - 下载 - - - - - - - - - - - - - diff --git a/src/router/manage.ts b/src/router/manage.ts index 32bd13f..bc49b4a 100644 --- a/src/router/manage.ts +++ b/src/router/manage.ts @@ -225,7 +225,7 @@ export default //管理页面 { path: 'tools/dynamic-nine-grid', name: 'ManageToolDynamicNineGrid', - component: () => import('@/components/manage/tools/ToolDynamicNineGrid.vue'), + component: () => import('@/views/manage/tools/ToolDynamicNineGrid.vue'), meta: { title: '动态九图生成器', parent: 'manage-tools-dashboard' // 指向工具箱仪表盘 diff --git a/src/views/manage/HistoryView.vue b/src/views/manage/HistoryView.vue index e2d6016..37ab023 100644 --- a/src/views/manage/HistoryView.vue +++ b/src/views/manage/HistoryView.vue @@ -15,8 +15,8 @@ import { } from 'echarts/components' import { use } from 'echarts/core' import { CanvasRenderer } from 'echarts/renderers' -import { NAlert, NButton, NCard, NDivider, NIcon, NSpace, NSpin, NText, NTime, NTooltip, useMessage } from 'naive-ui' -import { computed, onMounted, ref } from 'vue' +import { NAlert, NButton, NCard, NDatePicker, NDivider, NIcon, NSpace, NSpin, NText, NTime, NTooltip, useMessage } from 'naive-ui' +import { computed, onMounted, ref, watch } from 'vue' import VChart from 'vue-echarts' @@ -88,6 +88,26 @@ const isLoading = ref(true) const statisticStartDate = new Date(2023, 10, 4) const statisticStartDateTime = statisticStartDate.getTime() +// 日期范围选择(毫秒时间戳区间) +const dateRange = ref<[number, number] | null>(null) +const dateShortcuts: Record [number, number])> = { + '最近7天': () => { + const end = endOfDay(new Date()).getTime() + const start = startOfDay(addDays(new Date(), -6)).getTime() + return [start, end] as [number, number] + }, + '最近30天': () => { + const end = endOfDay(new Date()).getTime() + const start = startOfDay(addDays(new Date(), -29)).getTime() + return [start, end] as [number, number] + }, + '最近90天': () => { + const end = endOfDay(new Date()).getTime() + const start = startOfDay(addDays(new Date(), -89)).getTime() + return [start, end] as [number, number] + }, +} + // 响应式图表高度 const chartHeight = computed(() => { // 可以根据窗口大小动态调整图表高度 @@ -150,103 +170,99 @@ function getBaseChartOptions() { } } +/** + * 生成每日时间序列数据的通用函数 + * @param historyData 原始历史记录 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param initialTimeIndex 起始索引 + * @param initialCount 初始计数值 + */ +function generateTimeSeries( + historyData: HistoryRecordModel[], + startTime: Date, + endTime: Date, + initialTimeIndex: number, + initialCount: number +) { + const timeSeries: { time: Date; count: number; change: boolean; exist: boolean }[] = [] + let lastDayCount = initialCount + let lastTimeIndex = initialTimeIndex + let currentTime = startTime + + while (currentTime <= endTime) { + const dayEndTime = endOfDay(currentTime).getTime() + let dayExist = false + + while (true) { + const data = historyData[lastTimeIndex] + if (!data) { + break + } + + if ((historyData[lastTimeIndex + 1]?.time ?? Number.MAX_VALUE) > dayEndTime) { + const changed = data.count !== lastDayCount + lastDayCount = data.count + dayExist = true + break + } + lastTimeIndex++ + } + + timeSeries.push({ + time: currentTime, + count: lastDayCount, + change: lastDayCount !== (timeSeries[timeSeries.length - 1]?.count ?? initialCount), + exist: dayExist, + }) + + currentTime = addDays(currentTime, 1) + } + + return timeSeries +} + /** * 处理粉丝历史数据并生成图表选项 */ function processFansChartOptions() { if (!fansHistory.value || fansHistory.value.length === 0) return - // 确定开始时间 - let startTime = new Date(accountInfo.value?.createAt ?? Date.now()) - startTime = startTime < statisticStartDate ? statisticStartDate : startTime - startTime = startOfDay(startTime) - const endTime = new Date() + let startTimeBase = new Date(accountInfo.value?.createAt ?? Date.now()) + startTimeBase = startTimeBase < statisticStartDate ? statisticStartDate : startTimeBase + const startTime = startOfDay(dateRange.value ? new Date(dateRange.value[0]) : startTimeBase) + const endTime = dateRange.value ? new Date(dateRange.value[1]) : new Date() - // 用于存储完整的时间序列数据 - const completeTimeSeries: { time: Date; count: number; change: boolean, exist: boolean }[] = [] - // 用于存储粉丝增量数据 - const fansIncreacement: { time: Date; count: number }[] = [] - - // 查找统计开始时间之后的第一个数据点 - let lastFansTimeIndex = fansHistory.value.length > 0 - ? fansHistory.value[0].time >= statisticStartDateTime - ? 0 - : fansHistory.value.findIndex((entry) => entry.time >= statisticStartDateTime) - : -1 - let lastDayCount = lastFansTimeIndex >= 0 ? fansHistory.value[lastFansTimeIndex].count : 0 - - // 生成完整的天序列数据 - let currentTime = startTime - while (currentTime <= endTime) { - const dayEndTime = endOfDay(currentTime).getTime() - while (true) { - const data = fansHistory.value[lastFansTimeIndex] - if (!data) { - completeTimeSeries.push({ - time: currentTime, - count: lastDayCount, - change: false, - exist: false, - }) - break - } - // 如果下一个数据的时间大于当前天的结束时间 - if ((fansHistory.value[lastFansTimeIndex + 1]?.time ?? Number.MAX_VALUE) > dayEndTime) { - const changed = data.count !== lastDayCount - lastDayCount = data.count - - completeTimeSeries.push({ - time: currentTime, - count: lastDayCount, - change: changed, - exist: true, - }) - break - } - - lastFansTimeIndex++ - } - - currentTime = addDays(currentTime, 1) // 移动到下一天 + if (startTime > endTime) { + fansOption.value = { ...getBaseChartOptions(), series: [] } // Simplified empty state + return } - // 计算粉丝增量数据 - let previousDayCount = completeTimeSeries[0].count - completeTimeSeries.forEach((entry, index, array) => { - if (index === 0 || !isSameDay(entry.time, array[index - 1].time)) { - if (index > 0) { - const dailyIncrement = entry.count - previousDayCount - fansIncreacement.push({ - time: startOfDay(array[index - 1].time), - count: dailyIncrement, - }) - } - previousDayCount = entry.count - } else if (index === array.length - 1) { + const initialIndex = fansHistory.value.findIndex((entry) => entry.time >= statisticStartDateTime) + const initialCount = initialIndex >= 0 ? fansHistory.value[initialIndex].count : 0 + + const completeTimeSeries = generateTimeSeries(fansHistory.value, startTime, endTime, initialIndex, initialCount) + + const fansIncreacement: { time: Date; count: number }[] = [] + let previousDayCount = completeTimeSeries[0]?.count ?? 0 + completeTimeSeries.forEach((entry, index) => { + if (index > 0) { const dailyIncrement = entry.count - previousDayCount - fansIncreacement.push({ - time: startOfDay(entry.time), - count: dailyIncrement, - }) + fansIncreacement.push({ time: startOfDay(entry.time), count: dailyIncrement }) } + previousDayCount = entry.count }) - // 准备图表数据 const chartData = { xAxisData: completeTimeSeries.map((entry) => format(entry.time, 'yyyy-MM-dd')), - hourlyCounts: completeTimeSeries.map((entry) => entry.count), - dailyIncrements: fansIncreacement.map((entry) => ({ - date: format(entry.time, 'yyyy-MM-dd'), - count: entry.count, - })), + seriesData: completeTimeSeries.map((entry) => entry.count), + incrementData: fansIncreacement.map((entry) => entry.count), } - // 生成图表配置 - const baseOptions = getBaseChartOptions() fansOption.value = { - ...baseOptions, + ...getBaseChartOptions(), tooltip: { - ...baseOptions.tooltip, + ...getBaseChartOptions().tooltip, formatter: (param: any) => { const name = param[0].name + '' let str = '' @@ -260,54 +276,22 @@ function processFansChartOptions() { }, }, yAxis: [ - { - type: 'value', - name: '粉丝数', - }, - { - type: 'value', - name: '每日增量', - }, + { type: 'value', name: '粉丝数', min: 'dataMin' }, + { type: 'value', name: '每日增量' }, ], xAxis: [ - { - type: 'category', - axisTick: { - alignWithLabel: true, - }, - axisLine: { - onZero: false, - lineStyle: { - color: '#5470C6', - }, - }, - data: chartData.xAxisData, - }, - { - type: 'category', - axisTick: { - alignWithLabel: true, - }, - axisLine: { - onZero: false, - lineStyle: { - color: '#EE6666', - }, - }, - data: fansIncreacement.map((f) => format(f.time, 'yyyy-MM-dd')), - }, + { type: 'category', data: chartData.xAxisData }, + { type: 'category', data: chartData.xAxisData.slice(1) }, // Align increments ], series: [ { name: '粉丝数', type: 'line', - emphasis: { - focus: 'series', - }, - data: chartData.hourlyCounts, + data: chartData.seriesData, itemStyle: { - color: function (data: any) { - return completeTimeSeries[data.dataIndex].change ? '#18a058' : '#5470C6' + color: (data: any) => { + const item = completeTimeSeries[data.dataIndex] + return !item.exist ? '#cccccc' : item.change ? '#18a058' : '#5470C6' }, }, }, @@ -316,15 +300,8 @@ function processFansChartOptions() { type: 'bar', yAxisIndex: 1, xAxisIndex: 1, - emphasis: { - focus: 'series', - }, - data: chartData.dailyIncrements.map((f) => f.count), - itemStyle: { - color: function (params: any) { - return params.value < 0 ? '#FF4D4F' : '#3398DB' // 负数时红色,正数时默认颜色 - }, - }, + data: chartData.incrementData, + itemStyle: { color: (params: any) => (params.value < 0 ? '#FF4D4F' : '#3398DB') }, }, ], } @@ -336,120 +313,143 @@ function processFansChartOptions() { function processGuardsChartOptions() { if (!guardHistory.value || guardHistory.value.length === 0) return - // 确定开始时间 - let startTime = new Date(accountInfo.value?.createAt ?? Date.now()) - startTime = startTime < statisticStartDate ? statisticStartDate : startTime - startTime = startOfDay(startTime) - const endTime = new Date() + let startTimeBase = new Date(accountInfo.value?.createAt ?? Date.now()) + startTimeBase = startTimeBase < statisticStartDate ? statisticStartDate : startTimeBase + const startTime = startOfDay(dateRange.value ? new Date(dateRange.value[0]) : startTimeBase) + const endTime = dateRange.value ? new Date(dateRange.value[1]) : new Date() - // 生成完整的舰长天序列 - const completeGuardTimeSeries: { time: Date; count: number }[] = [] - let currentGuardTime = startTime - let lastGuardTimeIndex = 0 - let lastDayGuardCount = 0 - - while (currentGuardTime <= endTime) { - const dayEndTime = endOfDay(currentGuardTime).getTime() - while (true) { - const data = guardHistory.value[lastGuardTimeIndex] - if (!data) { - completeGuardTimeSeries.push({ - time: currentGuardTime, - count: lastDayGuardCount, - }) - break - } - - if ((guardHistory.value[lastGuardTimeIndex + 1]?.time ?? Number.MAX_VALUE) > dayEndTime) { - lastDayGuardCount = data.count - completeGuardTimeSeries.push({ - time: currentGuardTime, - count: lastDayGuardCount, - }) - break - } - - lastGuardTimeIndex++ - } - - currentGuardTime = addDays(currentGuardTime, 1) // 移动到下一天 + if (startTime > endTime) { + guardsOption.value = { ...getBaseChartOptions(), series: [] } // Simplified empty state + return } - // 计算守护增量数据 - const guardsIncreacement: { time: number; count: number; timeString: string }[] = [] - const guards: { time: number; count: number; timeString: string }[] = [] + const initialIndex = guardHistory.value.findIndex((entry) => entry.time >= startTime.getTime()) + const initialCount = initialIndex >= 0 ? guardHistory.value[initialIndex].count : 0 - let lastDayGuards = 0 - let lastDay = 0 + const completeTimeSeries = generateTimeSeries(guardHistory.value, startTime, endTime, initialIndex, initialCount) - completeGuardTimeSeries.forEach((g) => { - if (!isSameDay(g.time, new Date(lastDay * 1000))) { - guardsIncreacement.push({ - time: lastDayGuards, - count: lastDay === 0 ? 0 : g.count - lastDayGuards, - timeString: format(g.time, 'yyyy-MM-dd'), - }) - guards.push({ - time: g.time.getTime() / 1000, - count: g.count, - timeString: format(g.time, 'yyyy-MM-dd'), - }) - lastDay = g.time.getTime() / 1000 - lastDayGuards = g.count + const guardIncrements: number[] = [] + let previousDayCount = completeTimeSeries[0]?.count ?? 0 + completeTimeSeries.forEach((entry, index) => { + if (index > 0) { + guardIncrements.push(entry.count - previousDayCount) } + previousDayCount = entry.count }) - // 生成图表配置 - const baseOptions = getBaseChartOptions() + const xAxisData = completeTimeSeries.map((entry) => format(entry.time, 'yyyy-MM-dd')) + guardsOption.value = { - ...baseOptions, + ...getBaseChartOptions(), yAxis: [ - { - type: 'value', - }, - { - type: 'value', - }, + { type: 'value', name: '舰长数', min: 'dataMin' }, + { type: 'value', name: '日增' }, ], xAxis: [ - { - type: 'category', - axisTick: { - alignWithLabel: true, - }, - axisLine: { - onZero: false, - lineStyle: { - color: '#EE6666', - }, - }, - data: guardsIncreacement.map((f) => f.timeString), - }, + { type: 'category', data: xAxisData }, + { type: 'category', data: xAxisData.slice(1) }, ], series: [ { name: '舰长数', type: 'line', step: 'middle', - emphasis: { - focus: 'series', + data: completeTimeSeries.map(item => item.count), + itemStyle: { + color: (data: any) => completeTimeSeries[data.dataIndex].exist ? '#5470C6' : '#cccccc', }, - data: guards.map((f) => f.count), }, { name: '日增', type: 'bar', yAxisIndex: 1, - emphasis: { - focus: 'series', - }, - data: guardsIncreacement.map((f) => f.count), + xAxisIndex: 1, + data: guardIncrements, + itemStyle: { color: (params: any) => (params.value < 0 ? '#FF4D4F' : '#3398DB') }, + }, + ], + } +} + +/** + * 处理投稿数据并生成图表选项的通用函数 + * @param {('views' | 'likes')} dataType - 要处理的数据类型 ('views' 或 'likes') + * @param {string} title - 图表主标题 + */ +function processUpstatChartOptions(dataType: 'views' | 'likes', title: string) { + if (!upstatHistory.value || upstatHistory.value.length === 0) { + return { + ...getBaseChartOptions(), + xAxis: [{ type: 'category', data: [] }], + yAxis: [{ type: 'value' }, { type: 'value' }], + series: [ + { name: title, type: 'line', data: [] }, + { name: '日增', type: 'bar', data: [], yAxisIndex: 1 }, + ], + } + } + + const rangeStart = dateRange.value ? dateRange.value[0] : -Infinity + const rangeEnd = dateRange.value ? dateRange.value[1] : Infinity + const filtered = upstatHistory.value.filter((u) => u.time >= rangeStart && u.time <= rangeEnd) + + if (filtered.length === 0) { + return { + ...getBaseChartOptions(), + xAxis: [{ type: 'category', data: [] }], + yAxis: [{ type: 'value' }, { type: 'value' }], + series: [ + { name: title, type: 'line', data: [] }, + { name: '日增', type: 'bar', data: [], yAxisIndex: 1 }, + ], + } + } + + const increments: { time: number; value: number }[] = [] + let lastValue = filtered[0].stats[dataType] + + filtered.forEach((u) => { + const currentValue = u.stats[dataType] + increments.push({ + time: u.time, + value: currentValue - lastValue, + }) + lastValue = currentValue + }) + + return { + ...getBaseChartOptions(), + yAxis: [ + { type: 'value', min: 'dataMin' }, + { type: 'value' }, + ], + xAxis: [ + { + type: 'category', + axisTick: { alignWithLabel: true }, + axisLine: { onZero: false, lineStyle: { color: '#EE6666' } }, + data: filtered.map((f) => format(f.time, 'yyyy-MM-dd')), + }, + ], + series: [ + { + name: title, + type: 'line', + emphasis: { focus: 'series' }, + data: filtered.map((f) => f.stats[dataType]), itemStyle: { - color: function (params: any) { - return params.value < 0 ? '#FF4D4F' : '#3398DB' + color: function (data: any) { + return increments[data.dataIndex].value !== 0 ? '#5470C6' : '#cccccc' }, }, }, + { + name: '日增', + type: 'bar', + yAxisIndex: 1, + emphasis: { focus: 'series' }, + data: increments.map((f) => f.value), + }, ], } } @@ -458,134 +458,14 @@ function processGuardsChartOptions() { * 处理播放量历史数据并生成图表选项 */ function processUpstatViewChartOptions() { - if (!upstatHistory.value || upstatHistory.value.length === 0) return - - // 计算播放量增量数据 - const upstatViewIncreace: { time: number; value: number }[] = [] - let lastUpstatView = upstatHistory.value[0].stats.views - - upstatHistory.value.forEach((u) => { - upstatViewIncreace.push({ - time: u.time, - value: u.stats.views - lastUpstatView, - }) - lastUpstatView = u.stats.views - }) - - // 生成图表配置 - const baseOptions = getBaseChartOptions() - upstatViewOption.value = { - ...baseOptions, - yAxis: [ - { - type: 'value', - }, - { - type: 'value', - }, - ], - xAxis: [ - { - type: 'category', - axisTick: { - alignWithLabel: true, - }, - axisLine: { - onZero: false, - lineStyle: { - color: '#EE6666', - }, - }, - data: upstatHistory.value.map((f) => format(f.time, 'yyyy-MM-dd')), - }, - ], - series: [ - { - name: '播放数', - type: 'line', - emphasis: { - focus: 'series', - }, - data: upstatHistory.value.map((f) => f.stats.views), - }, - { - name: '日增', - type: 'bar', - yAxisIndex: 1, - emphasis: { - focus: 'series', - }, - data: upstatViewIncreace.map((f) => f.value), - }, - ], - } + upstatViewOption.value = processUpstatChartOptions('views', '播放数') } /** * 处理点赞量历史数据并生成图表选项 */ function processUpstatLikeChartOptions() { - if (!upstatHistory.value || upstatHistory.value.length === 0) return - - // 计算点赞量增量数据 - const upstatLikeIncreace: { time: number; value: number }[] = [] - let lastUpstatLike = upstatHistory.value[0].stats.likes - - upstatHistory.value.forEach((u) => { - upstatLikeIncreace.push({ - time: u.time, - value: u.stats.likes - lastUpstatLike, - }) - lastUpstatLike = u.stats.likes - }) - - // 生成图表配置 - const baseOptions = getBaseChartOptions() - upstatLikeOption.value = { - ...baseOptions, - yAxis: [ - { - type: 'value', - }, - { - type: 'value', - }, - ], - xAxis: [ - { - type: 'category', - axisTick: { - alignWithLabel: true, - }, - axisLine: { - onZero: false, - lineStyle: { - color: '#EE6666', - }, - }, - data: upstatHistory.value.map((f) => format(f.time, 'yyyy-MM-dd')), - }, - ], - series: [ - { - name: '点赞数', - type: 'line', - emphasis: { - focus: 'series', - }, - data: upstatHistory.value.map((f) => f.stats.likes), - }, - { - name: '日增', - type: 'bar', - yAxisIndex: 1, - emphasis: { - focus: 'series', - }, - data: upstatLikeIncreace.map((f) => f.value), - }, - ], - } + upstatLikeOption.value = processUpstatChartOptions('likes', '点赞数') } /** @@ -605,6 +485,14 @@ onMounted(async () => { isLoading.value = false } }) + +// 选择日期范围后,自动刷新图表 +watch( + () => dateRange.value, + () => { + if (!isLoading.value) processAllChartOptions() + } +) @@ -676,6 +564,17 @@ onMounted(async () => { + + 日期范围: + + + (null); + const newLinkName = ref(''); // 主页数据 const indexDisplayInfo = ref(); @@ -259,6 +261,15 @@ const addLinkName = ref(''); const addLinkUrl = ref(''); const linkKey = ref(0); + // 初始化 linkOrder (兼容旧数据) + onMounted(() => { + if (accountInfo.value?.settings?.index) { + const idx = accountInfo.value.settings.index; + if (!idx.linkOrder || idx.linkOrder.length === 0) { + idx.linkOrder = Object.keys(idx.links || {}); + } + } + }); /** * 获取B站用户数据 @@ -461,10 +472,56 @@ */ async function removeLink(name: string) { delete accountInfo.value.settings.index.links[name]; + if (accountInfo.value.settings.index.linkOrder) { + accountInfo.value.settings.index.linkOrder = accountInfo.value.settings.index.linkOrder.filter(k => k !== name); + } await updateIndexSettings(); location.reload(); } + /** 上移/下移视频 */ + function moveVideo(id: string, dir: 'up' | 'down') { + const list = accountInfo.value.settings.index.videos; + const i = list.indexOf(id); + if (i === -1) return; + const ni = dir === 'up' ? i - 1 : i + 1; + if (ni < 0 || ni >= list.length) return; + [list[i], list[ni]] = [list[ni], list[i]]; + updateIndexSettings(); + } + /** 上移/下移链接 */ + function moveLink(name: string, dir: 'up' | 'down') { + const order = accountInfo.value.settings.index.linkOrder; + if (!order) return; + const i = order.indexOf(name); + const ni = dir === 'up' ? i - 1 : i + 1; + if (i === -1 || ni < 0 || ni >= order.length) return; + [order[i], order[ni]] = [order[ni], order[i]]; + updateIndexSettings(); + linkKey.value++; + } + /** 编辑链接名称 */ + function startEditLink(name: string) { + editingLinkName.value = name; + newLinkName.value = name; + } + async function confirmEditLink(oldName: string) { + const idxSetting = accountInfo.value.settings.index; + if (!newLinkName.value || newLinkName.value === oldName) { + editingLinkName.value = null; return; + } + if (idxSetting.links[newLinkName.value]) { message.error('名称已存在'); return; } + idxSetting.links[newLinkName.value] = idxSetting.links[oldName]; + delete idxSetting.links[oldName]; + if (idxSetting.linkOrder) { + idxSetting.linkOrder = idxSetting.linkOrder.map(k => k === oldName ? newLinkName.value : k); + } + await updateIndexSettings(); + editingLinkName.value = null; + linkKey.value++; + } + function cancelEditLink() { editingLinkName.value = null; } + /** * 打开模板设置 */ @@ -719,12 +776,23 @@ > - - 删除 - + + 上移 + 下移 + 删除 + @@ -743,35 +811,75 @@ :key="linkKey" > - - - - {{ item[0] }} - - - {{ item[1] }} - - - + + + 保存 + 取消 + + + + + + {{ name }} + + + {{ indexDisplayInfo?.links[name] }} + + - - + @click="moveLink(name, 'up')" + >↑ + ↓ + 改名 + + + + + + + - - - 确定要删除这个链接吗? - + 确定要删除这个链接吗? + + + @@ -890,9 +998,6 @@ - - - 直播工具箱 - + @@ -12,7 +12,7 @@ diff --git a/src/views/manage/tools/DynamicNineGridGenerator.vue b/src/views/manage/tools/DynamicNineGridGenerator.vue deleted file mode 100644 index 903e2bf..0000000 --- a/src/views/manage/tools/DynamicNineGridGenerator.vue +++ /dev/null @@ -1,499 +0,0 @@ - - - 动态九宫格图片生成器 - - - 上传图片 - - - - 原图预览 - - - - - 裁剪区域 (选择一个正方形区域作为小图) - - 确认裁剪区域 - - - - - 单张小图预览 - - - - - 九宫格生成与自定义 - - 点击下方小图进行自定义内容添加。 - - - - 有自定义 - - - - - - - - 当前编辑: 第 {{ selectedCellIndex + 1 }} 张小图 - - - 添加自定义图片 - - - 已添加的自定义图片: - - - - - - 清空自定义图片 - - 应用自定义 - - - - - 生成并下载九宫格图片 - - 正在生成图片,请稍候... - - - - - - - \ No newline at end of file diff --git a/src/views/manage/tools/DynamicNineGridView.vue b/src/views/manage/tools/DynamicNineGridView.vue deleted file mode 100644 index bdd7350..0000000 --- a/src/views/manage/tools/DynamicNineGridView.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - diff --git a/src/views/manage/tools/ToolDynamicNineGrid.vue b/src/views/manage/tools/ToolDynamicNineGrid.vue index 53046a4..71c1d97 100644 --- a/src/views/manage/tools/ToolDynamicNineGrid.vue +++ b/src/views/manage/tools/ToolDynamicNineGrid.vue @@ -14,89 +14,133 @@ 请上传一张方形或长方形图片,将会自动分割成3x3的九宫格图片 - - - 请确保您上传的图片是方形或近似方形,以获得最佳效果 - - - - - 生成预览 - - 重新上传 - - - - 九宫格预览 - - 九宫格预览显示了图片分割后的效果,每个格子都可以添加下方图片 - - - - - - - {{ i }} + + + + + + + 手动裁剪 + + + + 自动中心裁剪 + 应用裁剪 + 重置裁剪 + + 重新选择图片 + + + + + + - - - - 第{{ i }}格 - - + + + 九宫格预览 + + 九宫格预览显示了图片分割后的效果,每个格子都可以添加下方图片 + + + + + + + {{ i }} + - - + + + + 导出设置: + + 单张边长 + + px + + + 格式 + + + + JPEG质量 + + + + + 清空所有下方图片 + + - - handleAdditionalImageChange(data, i - 1)" - accept="image/*" + + + - 添加下方图片 - - removeAdditionalImage(i-1)" - > - 删除下方图片 + + 第{{ i }}格 + + + + + + + + + handleAdditionalImageChange(data, i - 1)" + accept="image/*" + > + 添加下方图片 + + removeAdditionalImage(i-1)" + > + 删除下方图片 + + + + + + 生成九张图片 + + 打包下载 ZIP - - 生成九张图片 - - 打包下载全部 - - - + + 最终图片 以下是生成的九宫格图片,您可以单独下载每张图片 @@ -105,7 +149,7 @@ {{ index + 1 }} - downloadImage(imgDataUrl, `grid_image_${index + 1}.png`)" class="download-button"> + downloadImage(imgDataUrl, getFileName(index))" class="download-button"> 下载 @@ -118,39 +162,43 @@ diff --git a/src/views/view/indexTemplate/DefaultIndexTemplate.vue b/src/views/view/indexTemplate/DefaultIndexTemplate.vue index 4890791..d96fb41 100644 --- a/src/views/view/indexTemplate/DefaultIndexTemplate.vue +++ b/src/views/view/indexTemplate/DefaultIndexTemplate.vue @@ -7,7 +7,7 @@ import SimpleVideoCard from '@/components/SimpleVideoCard.vue'; import { defineTemplateConfig, ExtractConfigData } from '@/data/VTsuruConfigTypes'; import { USER_INDEX_API_URL } from '@/data/constants'; import { NAvatar, NButton, NCard, NDivider, NFlex, NSpace, NText, useMessage } from 'naive-ui'; -import { ref } from 'vue'; +import { ref, computed } from 'vue'; defineExpose({ Config, DefaultConfig }) const width = window.innerWidth @@ -23,6 +23,20 @@ const message = useMessage() const accountInfo = useAccount() const indexInfo = ref((await getIndexInfo()) || ({} as ResponseUserIndexModel)) +// 计算链接顺序(如果后端未提供 linkOrder 则使用对象键顺序) +const orderedLinks = computed(() => { + if (!indexInfo.value) return [] as [string, string][]; + const entries = Object.entries(indexInfo.value.links || {}); + if (!indexInfo.value.links) return []; + const order = (accountInfo.value?.settings?.index?.linkOrder?.length + ? accountInfo.value.settings.index.linkOrder + : (indexInfo.value as any)?.linkOrder) as string[] | undefined; + if (order && order.length) { + const map = new Map(entries); + return order.filter(k => map.has(k)).map(k => [k, map.get(k)!]) as [string, string][]; + } + return entries; +}); async function getIndexInfo() { try { isLoading.value = true @@ -123,9 +137,9 @@ export const Config = defineTemplateConfig([ - {{ userInfo.streamerInfo?.uId }} + UID: {{ userInfo.streamerInfo?.uId }} - 个人主页 - + >个人主页 - 直播间 - - - - - - {{ link[0] }} - - - + >直播间 + + 相关链接 + + + {{ link[0] }} + + + - - 相关视频 - + 相关视频