diff --git a/bun.lockb b/bun.lockb index 81fe540..c192cdf 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 7c84dcb..633e9ed 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@tauri-apps/plugin-updater": "^2.7.1", "@types/crypto-js": "^4.2.2", "@types/md5": "^2.3.5", + "@types/vue-cropperjs": "^4.1.6", "@vicons/fluent": "^0.13.0", "@vitejs/plugin-vue": "^5.2.3", "@vueuse/core": "^13.1.0", @@ -37,6 +38,7 @@ "@wangeditor/editor": "^5.1.23", "@wangeditor/editor-for-vue": "^5.1.12", "bilibili-live-ws": "^6.3.1", + "cropperjs": "^2.0.0", "crypto-js": "^4.2.0", "date-fns": "^4.1.0", "easy-speech": "^2.4.0", @@ -65,6 +67,7 @@ "vite-plugin-oxlint": "^1.3.1", "vite-svg-loader": "^5.1.0", "vue": "3.5.13", + "vue-cropperjs": "^5.0.0", "vue-echarts": "^7.0.3", "vue-request": "^2.0.4", "vue-router": "^4.5.1", diff --git a/src/components.d.ts b/src/components.d.ts index 9b557e5..9d3790b 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -53,6 +53,7 @@ declare module 'vue' { SongList: typeof import('./components/SongList.vue')['default'] SongPlayer: typeof import('./components/SongPlayer.vue')['default'] TempComponent: typeof import('./components/TempComponent.vue')['default'] + ToolDynamicNineGrid: typeof import('./components/manage/tools/ToolDynamicNineGrid.vue')['default'] TurnstileVerify: typeof import('./components/TurnstileVerify.vue')['default'] UpdateNoteContainer: typeof import('./components/UpdateNoteContainer.vue')['default'] UserBasicInfoCard: typeof import('./components/UserBasicInfoCard.vue')['default'] diff --git a/src/components/DynamicForm.vue b/src/components/DynamicForm.vue index 2563330..ba81781 100644 --- a/src/components/DynamicForm.vue +++ b/src/components/DynamicForm.vue @@ -414,7 +414,6 @@ import { computed, h, onMounted, ref } from 'vue'; function getItems() { } onMounted(() => { props.config?.forEach(item => { - console.log(props.configData) if (item.default && !(item.key in props.configData)) { props.configData[item.key] = item.default; } diff --git a/src/components/manage/PointGoodsItem.vue b/src/components/manage/PointGoodsItem.vue index 87a96e2..49e8fc2 100644 --- a/src/components/manage/PointGoodsItem.vue +++ b/src/components/manage/PointGoodsItem.vue @@ -160,6 +160,11 @@ import { NCard, NEllipsis, NEmpty, NFlex, NIcon, NImage, NTag, NText } from 'nai :line-clamp="2" class="description-text" > + +
+ + +
+ + 上传原始图片 + + + 请上传一张方形或长方形图片,将会自动分割成3x3的九宫格图片 + +
+
+ 请确保您上传的图片是方形或近似方形,以获得最佳效果 +
+
+ 原始图片 +
+ 生成预览 + + 重新上传 + +
+
+ 九宫格预览 + + 九宫格预览显示了图片分割后的效果,每个格子都可以添加下方图片 + + +
+ 完整预览 +
+
+
{{ i }}
+
+
+
+ +
+
+
+
第{{ i }}格
+
+ 九宫格预览 +
+
+
+ 附加图片 +
+
+ + 添加下方图片 + + + 删除下方图片 + +
+
+
+
+ 生成九张图片 + + 打包下载全部 + +
+
+ 最终图片 + + 以下是生成的九宫格图片,您可以单独下载每张图片 + +
+
+ +
{{ index + 1 }}
+ + 下载 + +
+
+
+ +
+
+
+ + + + + diff --git a/src/data/obsConstants.ts b/src/data/obsConstants.ts index 0a90ca4..1841340 100644 --- a/src/data/obsConstants.ts +++ b/src/data/obsConstants.ts @@ -36,4 +36,18 @@ export const OBSComponentMap: Record = { // settingName: 'obsExampleComponentSettings', // version: '1.0.0', // }, + Example: { + id: 'Example', + name: '示例组件', + description: '一个基础的OBS组件,用于演示和测试功能。', + component: defineAsyncComponent(() => import('@/views/obs_store/components/ExampleOBSComponent.vue')), + version: '1.0.0', + }, + Controller: { + id: 'Controller', + name: '控制器', + description: '将用户手柄操作映射到OBS的场景中', + component: defineAsyncComponent(() => import('@/views/obs_store/components/gamepads/GamepadViewer.vue')), + version: '1.0.0', + }, }; \ No newline at end of file diff --git a/src/router/manage.ts b/src/router/manage.ts index 350f9e8..32bd13f 100644 --- a/src/router/manage.ts +++ b/src/router/manage.ts @@ -93,6 +93,16 @@ export default //管理页面 danmaku: true } }, + { + path: 'obs-store', + name: 'manage-obsStore', + component: () => import('@/views/obs_store/OBSComponentStoreView.vue'), + meta: { + title: 'OBS组件库', + keepAlive: true, + danmaku: true + } + }, { path: 'queue', name: 'manage-liveQueue', @@ -202,6 +212,24 @@ export default //管理页面 meta: { title: '数据分析' } + }, + { + path: 'tools', + name: 'manage-tools-dashboard', + component: () => import('@/views/manage/ToolsDashboardView.vue'), + meta: { + title: '直播工具箱', + keepAlive: true + } + }, + { + path: 'tools/dynamic-nine-grid', + name: 'ManageToolDynamicNineGrid', + component: () => import('@/components/manage/tools/ToolDynamicNineGrid.vue'), + meta: { + title: '动态九图生成器', + parent: 'manage-tools-dashboard' // 指向工具箱仪表盘 + } } ] } diff --git a/src/router/obs_store.ts b/src/router/obs_store.ts index 822e026..82c61a5 100644 --- a/src/router/obs_store.ts +++ b/src/router/obs_store.ts @@ -5,7 +5,7 @@ export default { { path: 'gamepad-manage', name: 'obs-store-gamepad-manage', - component: () => import('@/views/manage/obs_store/components/gamepads/GamepadViewer.vue'), + component: () => import('@/views/obs_store/components/gamepads/GamepadViewer.vue'), meta: { title: '游戏手柄', forceReload: true, @@ -14,7 +14,7 @@ export default { { path: 'gamepad', name: 'obs-store-gamepad-display', - component: () => import('@/views/manage/obs_store/components/gamepads/GamepadDisplay.vue'), + component: () => import('@/views/obs_store/components/gamepads/GamepadDisplay.vue'), meta: { title: '手柄显示', forceReload: true, diff --git a/src/views/manage/SongListManageView.vue b/src/views/manage/SongListManageView.vue index 2f283b1..c948085 100644 --- a/src/views/manage/SongListManageView.vue +++ b/src/views/manage/SongListManageView.vue @@ -18,6 +18,8 @@ import { NAlert, NButton, NCheckbox, + NCollapse, + NCollapseItem, NDivider, NFlex, NForm, @@ -60,6 +62,18 @@ const showModal = ref(false) const showModalRenderKey = ref(0) const onlyResetNameOnAdded = ref(true) +// 文件导入的列头映射配置 +const useCustomColumnMapping = ref(false) +const columnMappings = useStorage('song-list-column-mappings', { + name: '名称,歌名,标题,title,name', + translateName: '翻译名称,译名,translated,translate', + author: '作者,歌手,演唱,singer,author,artist', + description: '描述,备注,说明,description,note,remark', + url: '链接,地址,url,link', + language: '语言,language', + tags: '标签,类别,分类,tag,tags,category' +}) + // 歌曲列表数据 const songs = ref([]) @@ -550,63 +564,81 @@ function parseExcelFile() { // 解析每一行数据 const parsedSongs = rows.map((row) => { - const song = {} as SongsInfo + const song = {} as SongsInfo; for (let i = 0; i < headers.length; i++) { - const key = headers[i] as string - const value = row[i] as string + const headerFromFile = (headers[i] as string)?.toLowerCase().trim(); + if (!headerFromFile) continue; - if (!key) continue + const value = row[i]; - // 根据列头映射到歌曲属性 - switch (key.toLowerCase().trim()) { - case 'id': - case 'name': - case '名称': - case '曲名': - case '歌名': - if (!value) { - console.log('忽略空歌名: ' + row) - continue - } - song.name = value - break - case 'author': - case 'singer': - case '作者': - case '歌手': - if (!value) break - song.author = parseMultipleValues(value) - break - case 'description': - case 'desc': - case '说明': - case '描述': - song.description = value - break - case 'url': - case '链接': - song.url = value - break - case 'language': - case '语言': - if (!value) break - song.language = parseMultipleValues(value) - break - case 'tags': - case 'tag': - case '标签': - if (!value) break - song.tags = parseMultipleValues(value) - break + // 歌曲名称 (必填) + const nameHeaders = columnMappings.value.name.split(/,|,/).map(h => h.trim().toLowerCase()); + if (nameHeaders.includes(headerFromFile)) { + if (value) song.name = value; + // 注意:即使找到歌名,也不立即continue,因为一个列可能对应多个信息(虽然不推荐) + // 但标准做法是每个信息有独立列 + } + + // 翻译名称 + if (columnMappings.value.translateName) { + const translateNameHeaders = columnMappings.value.translateName.split(/,|,/).map(h => h.trim().toLowerCase()); + if (translateNameHeaders.includes(headerFromFile)) { + if (value) song.translateName = value; + } + } + + // 作者 + if (columnMappings.value.author) { + const authorHeaders = columnMappings.value.author.split(/,|,/).map(h => h.trim().toLowerCase()); + if (authorHeaders.includes(headerFromFile)) { + if (value) song.author = parseMultipleValues(value as string); + } + } + + // 描述 + if (columnMappings.value.description) { + const descriptionHeaders = columnMappings.value.description.split(/,|,/).map(h => h.trim().toLowerCase()); + if (descriptionHeaders.includes(headerFromFile)) { + song.description = value; + } + } + + // 链接 + if (columnMappings.value.url) { + const urlHeaders = columnMappings.value.url.split(/,|,/).map(h => h.trim().toLowerCase()); + if (urlHeaders.includes(headerFromFile)) { + song.url = value; + } + } + + // 语言 + if (columnMappings.value.language) { + const languageHeaders = columnMappings.value.language.split(/,|,/).map(h => h.trim().toLowerCase()); + if (languageHeaders.includes(headerFromFile)) { + if (value) song.language = parseMultipleValues(value as string); + } + } + + // 标签 + if (columnMappings.value.tags) { + const tagsHeaders = columnMappings.value.tags.split(/,|,/).map(h => h.trim().toLowerCase()); + if (tagsHeaders.includes(headerFromFile)) { + if (value) song.tags = parseMultipleValues(value as string); + } } } - return song - }) + // 如果没有解析到歌名,则这条记录无效 + if (!song.name) { + console.log('忽略无效记录(未找到歌名或歌名为空): ' + row.join(',')); + return null; + } - // 过滤掉没有名称的歌曲 - uploadSongsFromFile.value = parsedSongs.filter((s) => s.name) + return song; + }).filter(s => s !== null) as SongsInfo[]; + + uploadSongsFromFile.value = parsedSongs; message.success('解析完成, 共获取 ' + uploadSongsFromFile.value.length + ' 首曲目') } } @@ -615,10 +647,9 @@ function parseExcelFile() { * 解析多值字段(如作者、标签等) */ function parseMultipleValues(value: string): string[] { - console.log(value) if (!value) return [] - // @ts-ignore - if (value instanceof Boolean) { + if (typeof value !== 'string') { + // @ts-ignore value = value.toString() } return value @@ -680,6 +711,31 @@ function resetAddingSong(onlyName = false) { message.success('已重置') } +/** + * 重置自定义列头映射 + */ +function resetColumnMappings() { + columnMappings.value = { + name: '名称,歌名,标题,title,name', + translateName: '翻译名称,译名,translated,translate', + author: '作者,歌手,演唱,singer,author,artist', + description: '描述,备注,说明,description,note,remark', + url: '链接,地址,url,link', + language: '语言,language', + tags: '标签,类别,分类,tag,tags,category' + } + message.success('已重置为默认映射') +} + +/** + * 保存自定义列头映射 + */ +function saveColumnMappings() { + // 由于使用了useStorage,映射内容会自动保存 + // 这里只需要提示用户保存成功 + message.success('映射已保存,下次导入将使用当前设置') +} + // 组件挂载时加载歌曲列表 onMounted(async () => { await getSongs() @@ -1144,6 +1200,99 @@ onMounted(async () => { 此页面 + + + 导入设置 + + + + + 自定义列头映射 + + + 启用后可以自定义Excel文件中列头与歌曲信息的对应关系 + + + + + + + + 请输入各字段对应的Excel列头名称,多个名称用逗号分隔。导入时会自动匹配这些名称,不区分大小写。 + + + + + + + + + + + + + + + + + + + + + + + + + 保存映射 + + + 重置为默认映射 + + + + 设置完成后请点击"保存映射",设置将自动保存到本地浏览器,下次访问时仍会使用 + + + + + + + + 文件上传 + + + + + 直播工具箱 + + + + + + + {{ tool.description }} + + + + + + + + + + + diff --git a/src/views/manage/ToolsManageView.vue b/src/views/manage/ToolsManageView.vue new file mode 100644 index 0000000..634d688 --- /dev/null +++ b/src/views/manage/ToolsManageView.vue @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/src/views/manage/obs_store/OBSComponentStoreView.vue b/src/views/manage/obs_store/OBSComponentStoreView.vue deleted file mode 100644 index 5575e43..0000000 --- a/src/views/manage/obs_store/OBSComponentStoreView.vue +++ /dev/null @@ -1,376 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/views/manage/obs_store/components/ExampleOBSComponent.vue b/src/views/manage/obs_store/components/ExampleOBSComponent.vue deleted file mode 100644 index 2042d03..0000000 --- a/src/views/manage/obs_store/components/ExampleOBSComponent.vue +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/views/manage/tools/DynamicNineGridGenerator.vue b/src/views/manage/tools/DynamicNineGridGenerator.vue new file mode 100644 index 0000000..903e2bf --- /dev/null +++ b/src/views/manage/tools/DynamicNineGridGenerator.vue @@ -0,0 +1,499 @@ + + + + + \ No newline at end of file diff --git a/src/views/manage/tools/DynamicNineGridView.vue b/src/views/manage/tools/DynamicNineGridView.vue new file mode 100644 index 0000000..bdd7350 --- /dev/null +++ b/src/views/manage/tools/DynamicNineGridView.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/src/views/manage/tools/ToolDynamicNineGrid.vue b/src/views/manage/tools/ToolDynamicNineGrid.vue new file mode 100644 index 0000000..53046a4 --- /dev/null +++ b/src/views/manage/tools/ToolDynamicNineGrid.vue @@ -0,0 +1,608 @@ + + + + + diff --git a/src/views/obs_store/OBSComponentStoreView.vue b/src/views/obs_store/OBSComponentStoreView.vue new file mode 100644 index 0000000..840c30e --- /dev/null +++ b/src/views/obs_store/OBSComponentStoreView.vue @@ -0,0 +1,421 @@ + + + + + \ No newline at end of file diff --git a/src/views/obs_store/components/ExampleOBSComponent.vue b/src/views/obs_store/components/ExampleOBSComponent.vue new file mode 100644 index 0000000..28ffd15 --- /dev/null +++ b/src/views/obs_store/components/ExampleOBSComponent.vue @@ -0,0 +1,182 @@ + + + + + \ No newline at end of file diff --git a/src/views/manage/obs_store/components/GamepadStick.vue b/src/views/obs_store/components/GamepadStick.vue similarity index 100% rename from src/views/manage/obs_store/components/GamepadStick.vue rename to src/views/obs_store/components/GamepadStick.vue diff --git a/src/views/manage/obs_store/components/gamepads/GamepadButton.vue b/src/views/obs_store/components/gamepads/GamepadButton.vue similarity index 100% rename from src/views/manage/obs_store/components/gamepads/GamepadButton.vue rename to src/views/obs_store/components/gamepads/GamepadButton.vue diff --git a/src/views/manage/obs_store/components/gamepads/GamepadDisplay.vue b/src/views/obs_store/components/gamepads/GamepadDisplay.vue similarity index 100% rename from src/views/manage/obs_store/components/gamepads/GamepadDisplay.vue rename to src/views/obs_store/components/gamepads/GamepadDisplay.vue diff --git a/src/views/manage/obs_store/components/gamepads/GamepadStick.vue b/src/views/obs_store/components/gamepads/GamepadStick.vue similarity index 100% rename from src/views/manage/obs_store/components/gamepads/GamepadStick.vue rename to src/views/obs_store/components/gamepads/GamepadStick.vue diff --git a/src/views/manage/obs_store/components/gamepads/GamepadViewer.vue b/src/views/obs_store/components/gamepads/GamepadViewer.vue similarity index 100% rename from src/views/manage/obs_store/components/gamepads/GamepadViewer.vue rename to src/views/obs_store/components/gamepads/GamepadViewer.vue