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"
>
+
+
+ {{ goods.description ? goods.description : '暂无描述' }}
+
+
+
+
+
+
+
+
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 @@
-
-
-
-
- {{ currentSelectedComponent ? currentSelectedComponent.description : '选择一个组件进行预览和配置' }}
-
-
-
-
- 配置组件
-
-
- 刷新组件
-
-
-
-
-
-
-
-
-
-
-
- {{ compDef.description }}
-
-
- v{{ compDef.version }}
-
-
-
-
-
-
-
-
- 正在加载组件配置和资源...
-
-
-
-
-
-
-
-
-
-
-
- 取消
-
-
- 保存配置
-
-
-
-
-
-
-
-
-
-
\ 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 @@
-
-
-
- {{ localConfig.contentText || '这是示例 OBS 组件的内容。' }}
-
- 当前用户: {{ userInfo.name }}
-
- 刷新次数: {{ refreshCount }}
- 当前配置:
{{ JSON.stringify(localConfig, null, 2) }}
-
-
-
-
-
-
-
- 更新标题
-
-
-
-
-
-
-
-
-
-
\ 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 @@
+
+
+
动态九宫格图片生成器
+
+
+ 上传图片
+
+
+
+
原图预览
+
![Original Image]()
+
+
+
+
裁剪区域 (选择一个正方形区域作为小图)
+
+
确认裁剪区域
+
+
+
+
+
单张小图预览
+
![Cropped Tile]()
+
+
+
+
九宫格生成与自定义
+
+ 点击下方小图进行自定义内容添加。
+
+
+
![]()
+
有自定义
+
+
+
+
+
+
+
+ 当前编辑: 第 {{ selectedCellIndex + 1 }} 张小图
+
+
+ 添加自定义图片
+
+
+ 已添加的自定义图片:
+
+
+
+
+
+ 清空自定义图片
+
+ 应用自定义
+
+
+
+
+ 生成并下载九宫格图片
+
+ 正在生成图片,请稍候...
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+
+ {{ currentSelectedComponent ? currentSelectedComponent.description : '选择一个组件进行预览和配置' }}
+
+
+
+
+
+
+
+
+
+ {{ compDef.description }}
+
+
+ v{{ compDef.version }}
+
+
+
+
+
+
+
+
+
+
+
+ 配置组件
+
+
+ 刷新组件
+
+
+
+
+
+ 正在加载组件配置和资源...
+
+
+
+
+
+ 占位
+
+
+
+
+
+
+
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+
+ 保存配置
+
+
+
+
+
+
+
+
+
+
\ 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 @@
+
+
+
+ {{ localConfig.contentText || '这是示例 OBS 组件的内容。' }}
+
+ 当前用户: {{ userInfo.name }}
+
+ 刷新次数: {{ refreshCount }}
+
+ 当前配置:
+
{{ JSON.stringify(localConfig, null, 2) }}
+
+
+
+
+
+
+
+
+ 更新标题
+
+
+
+
+
+
+
+
\ 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