From e1a19c48e0f24d9fc8d9b0aaa25daf0242093f7e Mon Sep 17 00:00:00 2001 From: rjl <98n9@163.com> Date: Fri, 30 Jan 2026 17:45:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E5=85=B8=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AD=97=E5=85=B8=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=9F=BA=E7=A1=80=E8=AE=BE?= =?UTF-8?q?=E6=96=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增字典管理页面,支持字典项的增删改查 - 添加 pinia-plugin-persistedstate 依赖,实现状态持久化 - 创建字典相关的 API 类型定义和服务接口 - 实现字典数据存储模块,支持字典数据的缓存和复用 - 添加 useDict 组合式函数,方便组件中使用字典数据 - 在题目分类和比赛创建模块中使用字典数据替代硬编码选项 - 优化分类树组件,添加拖拽手柄和题目类型选择 --- apps/admin/src/App.vue | 4 + apps/admin/src/hooks/business/useDict.ts | 29 +++ apps/admin/src/locales/langs/en-us.ts | 1 + apps/admin/src/locales/langs/zh-cn.ts | 1 + apps/admin/src/router/elegant/imports.ts | 1 + apps/admin/src/router/elegant/routes.ts | 11 + apps/admin/src/router/elegant/transform.ts | 1 + apps/admin/src/service/api/dictionary.ts | 59 +++++ apps/admin/src/store/index.ts | 2 + .../admin/src/store/modules/business/index.ts | 70 ++++++ apps/admin/src/typings/api/common.d.ts | 2 +- apps/admin/src/typings/api/dictionary.d.ts | 37 +++ apps/admin/src/typings/elegant-router.d.ts | 3 + .../competition/competition-add/index.vue | 9 +- .../modules/QuestionConfig.vue | 12 +- apps/admin/src/views/dictionary/index.vue | 218 ++++++++++++++++++ .../modules/DictionaryTypeModal.vue | 176 ++++++++++++++ apps/admin/src/views/question-store/index.vue | 4 +- .../question-store/modules/CategoryTree.vue | 27 ++- package.json | 3 + pnpm-lock.yaml | 24 ++ 21 files changed, 678 insertions(+), 16 deletions(-) create mode 100644 apps/admin/src/hooks/business/useDict.ts create mode 100644 apps/admin/src/service/api/dictionary.ts create mode 100644 apps/admin/src/store/modules/business/index.ts create mode 100644 apps/admin/src/typings/api/dictionary.d.ts create mode 100644 apps/admin/src/views/dictionary/index.vue create mode 100644 apps/admin/src/views/dictionary/modules/DictionaryTypeModal.vue diff --git a/apps/admin/src/App.vue b/apps/admin/src/App.vue index 089e43b..f40df32 100644 --- a/apps/admin/src/App.vue +++ b/apps/admin/src/App.vue @@ -4,6 +4,7 @@ import { darkTheme, NConfigProvider } from 'naive-ui' import { computed } from 'vue' import { naiveDateLocales, naiveLocales } from './locales/naive' import { useAppStore } from './store/modules/app' +import { useBusinessStore } from './store/modules/business' import { useThemeStore } from './store/modules/theme' defineOptions({ @@ -12,6 +13,9 @@ defineOptions({ const appStore = useAppStore() const themeStore = useThemeStore() +const businessStore = useBusinessStore() +/* 初始化字典 */ +businessStore.initDict() const naiveDarkTheme = computed(() => (themeStore.darkMode ? darkTheme : undefined)) diff --git a/apps/admin/src/hooks/business/useDict.ts b/apps/admin/src/hooks/business/useDict.ts new file mode 100644 index 0000000..5024987 --- /dev/null +++ b/apps/admin/src/hooks/business/useDict.ts @@ -0,0 +1,29 @@ +import { computed, onMounted } from 'vue' +import { useBusinessStore } from '@/store/modules/business' + +export function useDict(key: string) { + const store = useBusinessStore() + + const data = computed(() => store.dictData[key] || []) + const loading = computed(() => store.loadingMap[key] || false) + + const options = computed(() => { + if (data.value && data.value.length > 0) { + return data.value.map((item: any) => ({ + label: item.uI_Key || item.UI_Key || item.DicKey, + value: item.uI_Value || item.UI_Value || item.DicValue, + })) + } + return [] + }) + + onMounted(() => { + store.getDict(key) + }) + + return { + data, + options, + loading, + } +} diff --git a/apps/admin/src/locales/langs/en-us.ts b/apps/admin/src/locales/langs/en-us.ts index 536d1f9..1346a76 100644 --- a/apps/admin/src/locales/langs/en-us.ts +++ b/apps/admin/src/locales/langs/en-us.ts @@ -242,6 +242,7 @@ const local: App.I18n.Schema = { 'competition_competition-add': 'Competition Add', 'competition_competition-detail': 'Competition Detail', 'competition_competition-list': 'Competition List', + 'dictionary': 'Dictionary', }, page: { login: { diff --git a/apps/admin/src/locales/langs/zh-cn.ts b/apps/admin/src/locales/langs/zh-cn.ts index 615af0f..9f8bc17 100644 --- a/apps/admin/src/locales/langs/zh-cn.ts +++ b/apps/admin/src/locales/langs/zh-cn.ts @@ -238,6 +238,7 @@ const local: App.I18n.Schema = { 'competition_competition-add': '比赛添加', 'competition_competition-detail': '比赛详情', 'competition_competition-list': '比赛列表', + 'dictionary': '字典管理', }, page: { login: { diff --git a/apps/admin/src/router/elegant/imports.ts b/apps/admin/src/router/elegant/imports.ts index b5f51fa..4a91a9f 100644 --- a/apps/admin/src/router/elegant/imports.ts +++ b/apps/admin/src/router/elegant/imports.ts @@ -23,6 +23,7 @@ export const views: Record Promise import("@/views/competition/competition-add/index.vue"), "competition_competition-detail": () => import("@/views/competition/competition-detail/index.vue"), "competition_competition-list": () => import("@/views/competition/competition-list/index.vue"), + dictionary: () => import("@/views/dictionary/index.vue"), home: () => import("@/views/home/index.vue"), "question-store": () => import("@/views/question-store/index.vue"), "rank_rank-detail": () => import("@/views/rank/rank-detail/index.vue"), diff --git a/apps/admin/src/router/elegant/routes.ts b/apps/admin/src/router/elegant/routes.ts index 1034fc1..3d52f5e 100644 --- a/apps/admin/src/router/elegant/routes.ts +++ b/apps/admin/src/router/elegant/routes.ts @@ -83,6 +83,17 @@ export const generatedRoutes: GeneratedRoute[] = [ } ] }, + { + name: 'dictionary', + path: '/dictionary', + component: 'layout.base$view.dictionary', + meta: { + title: 'dictionary', + i18nKey: 'route.dictionary', + icon: 'material-symbols:settings', + order: 6 + } + }, { name: 'home', path: '/home', diff --git a/apps/admin/src/router/elegant/transform.ts b/apps/admin/src/router/elegant/transform.ts index 32a6320..abc66ea 100644 --- a/apps/admin/src/router/elegant/transform.ts +++ b/apps/admin/src/router/elegant/transform.ts @@ -170,6 +170,7 @@ const routeMap: RouteMap = { "competition_competition-add": "/competition/competition-add", "competition_competition-detail": "/competition/competition-detail", "competition_competition-list": "/competition/competition-list", + "dictionary": "/dictionary", "home": "/home", "iframe-page": "/iframe-page/:url", "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?", diff --git a/apps/admin/src/service/api/dictionary.ts b/apps/admin/src/service/api/dictionary.ts new file mode 100644 index 0000000..6783c94 --- /dev/null +++ b/apps/admin/src/service/api/dictionary.ts @@ -0,0 +1,59 @@ +import { request } from '../request' + +/** Get dictionary list */ +export function getDictionaryList(roomID?: number) { + return request({ + url: '/Base/ActivityMain/GetDictionaryList', + method: 'get', + params: { + RoomId: roomID, + }, + }) +} + +/** 新增字段项目 */ +export function addOrUpdateDictionary(data: Api.Dictionary.AddOrUpdateParams) { + return request({ + url: '/Base/ActivityMain/AddOrUpdateDictionary', + method: 'post', + data, + }) +} + +/** Delete dictionary */ +export function deleteDictionary(DicID: number) { + return request({ + url: `/Base/ActivityMain/DeleteDictionary/?DicID=${DicID}`, + method: 'post', + data: { DicID }, + }) +} + +/** 根据字典ID查询字典类型 */ +export function getDictionaryListUITypeByDicID(dicID: number) { + return request({ + url: '/Base/ActivityMain/GetDictionaryListUITypeByDicID', + method: 'get', + params: { + DicID: dicID, + }, + }) +} + +/** 根据字典ID新建下面的字典类型 */ +export function addOrUpdateDictionaryUIType(data: Api.Dictionary.AddOrUpdateDictionaryUITypeParams) { + return request({ + url: '/Base/ActivityMain/AddOrUpdateDictionaryUIType', + method: 'post', + data, + }) +} + +/** 删除字典项 */ +export function deleteDictionaryItem(DicUIID: number) { + return request({ + url: `/Base/ActivityMain/DeleteDictionaryUIType/?DicUIID=${DicUIID}`, + method: 'post', + data: { DicUIID }, + }) +} diff --git a/apps/admin/src/store/index.ts b/apps/admin/src/store/index.ts index bcf3694..ad51ec7 100644 --- a/apps/admin/src/store/index.ts +++ b/apps/admin/src/store/index.ts @@ -1,5 +1,6 @@ import type { App } from 'vue' import { createPinia } from 'pinia' +import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import { resetSetupStore } from './plugins' /** Setup Vue store plugin pinia */ @@ -7,6 +8,7 @@ export function setupStore(app: App) { const store = createPinia() store.use(resetSetupStore) + store.use(piniaPluginPersistedstate) app.use(store) } diff --git a/apps/admin/src/store/modules/business/index.ts b/apps/admin/src/store/modules/business/index.ts new file mode 100644 index 0000000..14aa7d4 --- /dev/null +++ b/apps/admin/src/store/modules/business/index.ts @@ -0,0 +1,70 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { getDictionaryList, getDictionaryListUITypeByDicID } from '@/service/api/dictionary' + +export const useBusinessStore = defineStore('business-store', () => { + const dictList = ref([]) + const dictData = ref>({}) + const isInitialized = ref(false) + const loadingMap = ref>({}) + + /** 初始化字典 */ + async function initDict() { + if (isInitialized.value) + return + const { data: res, error } = await getDictionaryList() + if (!error && res) { + dictList.value = res.data || [] + isInitialized.value = true + } + } + + /** 获取字典数据 */ + async function getDict(key: string) { + if (!isInitialized.value) { + await initDict() + } + + // 已经加载过,直接返回 + if (dictData.value[key] && dictData.value[key].length > 0) + return dictData.value[key] + + // 查找 ID + const dict = dictList.value.find(d => d.DicValue === key) + if (!dict) { + console.warn(`Dictionary key ${key} not found`) + return [] + } + + if (loadingMap.value[key]) { + return [] + } + + loadingMap.value[key] = true + try { + const { data: res, error } = await getDictionaryListUITypeByDicID(dict.Id) + if (!error && res) { + const list = res.data || [] + // 使用新对象赋值,确保触发响应式更新 + dictData.value = { + ...dictData.value, + [key]: list, + } + } + } + finally { + loadingMap.value[key] = false + } + // eslint-disable-next-line no-console + console.log(`获取字典数据 ${key}`, dictData.value[key]) + return dictData.value[key] + } + + return { + dictList, + dictData, + loadingMap, + initDict, + getDict, + } +}, { persist: true }) diff --git a/apps/admin/src/typings/api/common.d.ts b/apps/admin/src/typings/api/common.d.ts index 73c3e24..fbfc2f4 100644 --- a/apps/admin/src/typings/api/common.d.ts +++ b/apps/admin/src/typings/api/common.d.ts @@ -24,7 +24,7 @@ declare namespace Api { type CommonSearchParams = Pick /** common response */ - interface CommonResponse { + export interface CommonResponse { success: boolean code: number msg: string diff --git a/apps/admin/src/typings/api/dictionary.d.ts b/apps/admin/src/typings/api/dictionary.d.ts new file mode 100644 index 0000000..5d2b4f4 --- /dev/null +++ b/apps/admin/src/typings/api/dictionary.d.ts @@ -0,0 +1,37 @@ +/** + * Namespace Api + * + * All backend api type + */ +declare namespace Api { + namespace Dictionary { + interface DictionaryItem { + Id: number + DicKey: string + DicValue: string + bakValue: string + } + + interface DictionaryUIItem { + id: number + uI_Key: string + uI_Value: string + bakValue: string + dictionaryID: number + } + + type AddOrUpdateParams = DictionaryItem + + interface AddOrUpdateDictionaryUITypeParams { + id?: number + uI_Key: string + uI_Value: string + bakValue: string + dictionaryID: number + } + + interface SearchParams { + RoomID: number + } + } +} diff --git a/apps/admin/src/typings/elegant-router.d.ts b/apps/admin/src/typings/elegant-router.d.ts index b3da2c6..88840d6 100644 --- a/apps/admin/src/typings/elegant-router.d.ts +++ b/apps/admin/src/typings/elegant-router.d.ts @@ -24,6 +24,7 @@ declare module "@elegant-router/types" { "competition_competition-add": "/competition/competition-add"; "competition_competition-detail": "/competition/competition-detail"; "competition_competition-list": "/competition/competition-list"; + "dictionary": "/dictionary"; "home": "/home"; "iframe-page": "/iframe-page/:url"; "login": "/login/:module(pwd-login|code-login|register|reset-pwd|bind-wechat)?"; @@ -71,6 +72,7 @@ declare module "@elegant-router/types" { | "404" | "500" | "competition" + | "dictionary" | "home" | "iframe-page" | "login" @@ -103,6 +105,7 @@ declare module "@elegant-router/types" { | "competition_competition-add" | "competition_competition-detail" | "competition_competition-list" + | "dictionary" | "home" | "question-store" | "rank_rank-detail" diff --git a/apps/admin/src/views/competition/competition-add/index.vue b/apps/admin/src/views/competition/competition-add/index.vue index dd1660c..0288100 100644 --- a/apps/admin/src/views/competition/competition-add/index.vue +++ b/apps/admin/src/views/competition/competition-add/index.vue @@ -1,7 +1,7 @@ + + + + diff --git a/apps/admin/src/views/dictionary/modules/DictionaryTypeModal.vue b/apps/admin/src/views/dictionary/modules/DictionaryTypeModal.vue new file mode 100644 index 0000000..ced8308 --- /dev/null +++ b/apps/admin/src/views/dictionary/modules/DictionaryTypeModal.vue @@ -0,0 +1,176 @@ + + + diff --git a/apps/admin/src/views/question-store/index.vue b/apps/admin/src/views/question-store/index.vue index 9ade057..1c5f35a 100644 --- a/apps/admin/src/views/question-store/index.vue +++ b/apps/admin/src/views/question-store/index.vue @@ -50,10 +50,10 @@ function onMouseUp() {
- +
diff --git a/apps/admin/src/views/question-store/modules/CategoryTree.vue b/apps/admin/src/views/question-store/modules/CategoryTree.vue index dcfdf88..0bcf7e3 100644 --- a/apps/admin/src/views/question-store/modules/CategoryTree.vue +++ b/apps/admin/src/views/question-store/modules/CategoryTree.vue @@ -10,12 +10,16 @@ import { } from 'naive-ui' import { onMounted, ref, watch } from 'vue' import SvgIcon from '@/components/custom/svg-icon.vue' +import { useDict } from '@/hooks/business/useDict' import { fetchAddQuestion, fetchDeleteQuestion, fetchGetQuestionListAll, fetchUpdateQuestion } from '@/service/api/question' const emit = defineEmits<{ (e: 'update:category', node: any): void + (e: 'dragStart', event: MouseEvent): void }>() +const { options: questionTypeOptions } = useDict('question_type') + const message = useMessage() const dialog = useDialog() @@ -30,9 +34,8 @@ const categoryList = ref([]) const selectedKey = ref(null) -// Modal State const showCategoryModal = ref(false) -const categoryForm = ref({ questionContent: '', name: '', Id: 0 }) +const categoryForm = ref({ questionContent: '', name: '', Id: 0, questionType: 'SingleChoice' }) const categoryOperation = ref<'add' | 'edit'>('add') const currentOperationNode = ref(null) @@ -170,7 +173,7 @@ onMounted(() => { + + +
+ +
- - diff --git a/package.json b/package.json index e50db69..fc6cc9b 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,9 @@ "prepare": "simple-git-hooks", "typecheck": "pnpm -r typecheck" }, + "dependencies": { + "pinia-plugin-persistedstate": "^4.7.1" + }, "devDependencies": { "@read-star/eslint-config": "workspace:*", "eslint": "latest", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24eb5c7..38ea139 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + pinia-plugin-persistedstate: + specifier: ^4.7.1 + version: 4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))) devDependencies: '@read-star/eslint-config': specifier: workspace:* @@ -4064,6 +4068,20 @@ packages: engines: {node: '>=0.10'} hasBin: true + pinia-plugin-persistedstate@4.7.1: + resolution: {integrity: sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==} + peerDependencies: + '@nuxt/kit': '>=3.0.0' + '@pinia/nuxt': '>=0.10.0' + pinia: '>=3.0.0' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@pinia/nuxt': + optional: true + pinia: + optional: true + pinia@3.0.4: resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} peerDependencies: @@ -9362,6 +9380,12 @@ snapshots: pidtree@0.6.0: {} + pinia-plugin-persistedstate@4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))): + dependencies: + defu: 6.1.4 + optionalDependencies: + pinia: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) + pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)): dependencies: '@vue/devtools-api': 7.7.9