From 133d9a903a11cf19bf32e92d06f31c4b957e582d Mon Sep 17 00:00:00 2001 From: rjl <98n9@163.com> Date: Wed, 28 Jan 2026 08:33:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=9B=B4=E6=96=B0=E9=A2=98?= =?UTF-8?q?=E5=BA=93=E7=AE=A1=E7=90=86=E5=92=8C=E7=8E=AF=E5=A2=83=E9=85=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 禁用强制登录以便本地开发 - 更新开发环境API基础URL为本地服务 - 在代理配置中添加secure: false以支持HTTPS接口 - 修复请求响应中字符串类型数据的JSON解析问题 - 新增题目类型相关常量定义和类型声明 - 扩展transformRecordToOption函数支持数字键值 - 更新题库相关API接口和类型定义 - 在竞赛配置中添加题目分数字段 - 重构题库管理界面,实现分类和题目的完整CRUD操作 - 删除未使用的模板管理组件 --- apps/admin/.env | 2 +- apps/admin/.env.dev | 4 +- apps/admin/build/config/proxy.ts | 1 + apps/admin/src/constants/business.ts | 7 + apps/admin/src/service/api/question.ts | 90 ++++- apps/admin/src/service/request/index.ts | 7 +- apps/admin/src/typings/api/question.d.ts | 40 +- apps/admin/src/typings/components.d.ts | 2 + apps/admin/src/utils/common.ts | 21 +- .../modules/QuestionConfig.vue | 15 +- .../question-store/modules/CategoryTree.vue | 134 ++++--- .../question-store/modules/QuestionList.vue | 376 +++++++++++------- .../modules/template-operate-drawer.vue | 147 ------- .../template/modules/template-search.vue | 92 ----- 14 files changed, 484 insertions(+), 454 deletions(-) delete mode 100644 apps/admin/src/views/template/modules/template-operate-drawer.vue delete mode 100644 apps/admin/src/views/template/modules/template-search.vue diff --git a/apps/admin/.env b/apps/admin/.env index 63afcaf..1dc05b6 100644 --- a/apps/admin/.env +++ b/apps/admin/.env @@ -25,7 +25,7 @@ VITE_ICON_LOCAL_PREFIX=icon-local VITE_AUTH_ROUTE_MODE=static # 是否强制登录: Y (开启) | N (关闭) -VITE_AUTH_ROUTE_FORCE_LOGIN=Y +VITE_AUTH_ROUTE_FORCE_LOGIN=N # 静态认证路由的主页 VITE_ROUTE_HOME=home diff --git a/apps/admin/.env.dev b/apps/admin/.env.dev index a26e12c..6eea838 100644 --- a/apps/admin/.env.dev +++ b/apps/admin/.env.dev @@ -3,9 +3,9 @@ # VITE_SERVICE_BASE_URL=http://192.168.5.140:9999 -VITE_SERVICE_BASE_URL=https://api.qyzhjy.com +# VITE_SERVICE_BASE_URL=https://api.qyzhjy.com -# VITE_SERVICE_BASE_URL=http://172.16.10.130:5000 +VITE_SERVICE_BASE_URL=https://172.16.10.130:5000 VITE_OTHER_SERVICE_BASE_URL= `{ "demo": "http://localhost:9528" diff --git a/apps/admin/build/config/proxy.ts b/apps/admin/build/config/proxy.ts index 5bd3a8c..0e7d88e 100644 --- a/apps/admin/build/config/proxy.ts +++ b/apps/admin/build/config/proxy.ts @@ -40,6 +40,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean proxy[item.proxyPattern] = { target: item.baseURL, changeOrigin: true, + secure: false, // 如果是https接口,需要配置这个参数 configure: (_proxy, options) => { _proxy.on('proxyReq', (_proxyReq, req, _res) => { if (!enableLog) diff --git a/apps/admin/src/constants/business.ts b/apps/admin/src/constants/business.ts index 427e9bb..261fe4f 100644 --- a/apps/admin/src/constants/business.ts +++ b/apps/admin/src/constants/business.ts @@ -63,6 +63,13 @@ export const timeRecord: Record = { 90: '90s', } +export const questionTypeRecord: Record = { + 0: '文字', + 1: '图片', +} + +export const questionTypeOptions: SelectOption[] = transformRecordToOption(questionTypeRecord, true) as unknown as SelectOption[] + export const timeOptions: SelectOption[] = transformRecordToOption(timeRecord) as unknown as SelectOption[] export const uiTypeOptions: SelectOption[] = transformRecordToOption(uiTypeRecord) as unknown as SelectOption[] diff --git a/apps/admin/src/service/api/question.ts b/apps/admin/src/service/api/question.ts index e1736b8..f40fd25 100644 --- a/apps/admin/src/service/api/question.ts +++ b/apps/admin/src/service/api/question.ts @@ -1,16 +1,13 @@ import { request } from '../request' /** 获取所有题目列表 */ -export function fetchGetQuestionListAll(params?: any) { - return request({ +export function fetchGetQuestionListAll() { + return request({ url: '/Base/ActivityMain/GetQuestionListAll', method: 'get', - params: params || {}, }) } - - /** 新增题目 */ export function fetchAddQuestion(data?: Api.Question.AddParams) { return request({ @@ -19,3 +16,86 @@ export function fetchAddQuestion(data?: Api.Question.AddParams) { data: data || {}, }) } + +/** 更新题目 */ +export function fetchUpdateQuestion(data?: Api.Question.AddParams) { + return request({ + url: '/Base/ActivityMain/UpdateQuestionList', + method: 'post', + data: data || {}, + }) +} + +/** 删除题目 */ +export function fetchDeleteQuestion(id: number) { + return request({ + url: `/Base/ActivityMain/DeleteQuestionList/?Id=${id}`, + method: 'post', + }) +} + +/** 获取所有问题库列表(分页) */ +export function fetchGetQuestionLibraryListAll(params: Api.Question.GetQuestionLibraryListAllParams) { + return request({ + url: '/Base/ActivityMain/GetQuestionListDetail', + method: 'get', + params: { + QuestionId: params.questionId, + PageIndex: params.pageIndex, + PageSizes: params.pageSizes, + keyWords: params.keyWords, + }, + }) +} + +/** 新增一条问题库 */ +export function fetchAddQuestionLibrary(params: Api.Question.AddQuestionLibraryParams, file?: File | null) { + const formData = new FormData() + if (file) { + formData.append('Image', file) + } + return request({ + url: '/Base/ActivityMain/AddQuestionListDetail', + method: 'post', + params: { + QuestionId: params.questionId, + Name: params.name, + Answer: params.answer, + Type: params.type, + IsGood: params.IsGood, + ImageUrl: params.imageUrl, + }, + data: formData, + }) +} + +/** 更新一条问题库 */ +export function fetchUpdateQuestionLibrary(params: Api.Question.AddQuestionLibraryParams, file?: File | null) { + const formData = new FormData() + if (file) { + formData.append('Image', file) + } + return request({ + url: '/Base/ActivityMain/UpdateQuestionListDetail', + method: 'post', + params: { + Id: params.id, + QuestionId: params.questionId, + Name: params.name, + Answer: params.answer, + Type: params.type, + IsGood: params.IsGood, + ImageUrl: params.imageUrl, + }, + data: formData, + }) +} + +/** 删除一条问题库 */ +export function fetchDeleteQuestionLibrary(ids: number[]) { + return request({ + url: `/Base/ActivityMain/DeleteQuestionListDetail`, + method: 'post', + data: ids, + }) +} diff --git a/apps/admin/src/service/request/index.ts b/apps/admin/src/service/request/index.ts index 6facc60..3027c95 100644 --- a/apps/admin/src/service/request/index.ts +++ b/apps/admin/src/service/request/index.ts @@ -28,7 +28,12 @@ export const request = createFlatRequest( } as RequestInstanceState, // 响应数据转换 transform(response: AxiosResponse>) { - return response.data.data + // 如果后端返回的data 是string 类型,需要转换为对象 + if (typeof response.data.data === 'string') { + response.data.data = JSON.parse(response.data.data) + return response.data + } + return response.data }, // 请求配置转换 async onRequest(config) { diff --git a/apps/admin/src/typings/api/question.d.ts b/apps/admin/src/typings/api/question.d.ts index 7f30fc0..19be872 100644 --- a/apps/admin/src/typings/api/question.d.ts +++ b/apps/admin/src/typings/api/question.d.ts @@ -7,29 +7,59 @@ declare namespace Api { namespace Question { /** common params of paginating */ interface PaginatingCommonParams { - /** current page number */ - current: number + /** pageIndex page number */ + pageIndex: number /** page size */ - size: number + pageSizes: number /** total count */ total: number } /** common params of paginating query list data */ interface PaginatingQueryRecord extends PaginatingCommonParams { - records: T[] + data: T[] + } + + /** get question library list all params */ + interface GetQuestionLibraryListAllParams { + /** question id */ + questionId: number + /** pageIndex page number */ + pageIndex: number + /** page size */ + pageSizes: number + /** search keyword */ + keyWords?: string } /** add params */ interface AddParams { /** question id */ - id: string + id: number /** question name */ name: string /** question content */ questionContent: string } + /** add question library params */ + interface AddQuestionLibraryParams { + /** question library id */ + id: number + /** question library name */ + name: string + /** question library content */ + answer: string + /** question id */ + questionId: number + /** question library image url */ + imageUrl: string + /** question library type */ + type: QuestionScoreType + /** is priority 0: no 1: yes */ + IsGood: number + } + /** common search params of table */ type CommonSearchParams = Pick diff --git a/apps/admin/src/typings/components.d.ts b/apps/admin/src/typings/components.d.ts index 05830f0..daaf174 100644 --- a/apps/admin/src/typings/components.d.ts +++ b/apps/admin/src/typings/components.d.ts @@ -131,6 +131,7 @@ declare module 'vue' { NRadioGroup: typeof import('naive-ui')['NRadioGroup'] NScrollbar: typeof import('naive-ui')['NScrollbar'] NSelect: typeof import('naive-ui')['NSelect'] + NSelectOption: typeof import('naive-ui')['NSelectOption'] NSpace: typeof import('naive-ui')['NSpace'] NSpin: typeof import('naive-ui')['NSpin'] NStatistic: typeof import('naive-ui')['NStatistic'] @@ -290,6 +291,7 @@ declare global { const NRadioGroup: typeof import('naive-ui')['NRadioGroup'] const NScrollbar: typeof import('naive-ui')['NScrollbar'] const NSelect: typeof import('naive-ui')['NSelect'] + const NSelectOption: typeof import('naive-ui')['NSelectOption'] const NSpace: typeof import('naive-ui')['NSpace'] const NSpin: typeof import('naive-ui')['NSpin'] const NStatistic: typeof import('naive-ui')['NStatistic'] diff --git a/apps/admin/src/utils/common.ts b/apps/admin/src/utils/common.ts index 95945c8..403c08c 100644 --- a/apps/admin/src/utils/common.ts +++ b/apps/admin/src/utils/common.ts @@ -17,10 +17,11 @@ import { $t } from '@/locales' * ```; * * @param record + * @param isNumberKey */ -export function transformRecordToOption>(record: T) { +export function transformRecordToOption>(record: T, isNumberKey = false) { return Object.entries(record).map(([value, label]) => ({ - value, + value: isNumberKey ? Number(value) : value, label, })) as CommonType.Option[] } @@ -67,8 +68,8 @@ export function browserPathJoin(...paths: string[]) { /** * 版本比较 - * @param v1 - * @param v2 + * @param v1 + * @param v2 * @param separator 分隔符 默认. * @returns 1 v1>v2 | -1 v1 num2) return 1 - if (num1 < num2) return -1 + const num1 = Number.parseInt(s1[i] || '0') + const num2 = Number.parseInt(s2[i] || '0') + if (num1 > num2) + return 1 + if (num1 < num2) + return -1 } return 0 } diff --git a/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue b/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue index 9a3fbdb..734a9ac 100644 --- a/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue +++ b/apps/admin/src/views/competition/competition-add/modules/QuestionConfig.vue @@ -17,6 +17,7 @@ const props = defineProps<{ time: number title: string scoreType: Api.Competition.QuestionScoreType + score: number uiType?: string templateId?: string }> @@ -292,7 +293,7 @@ defineExpose({ validate, reset }) -
+
答题时间: -
+
分数规则: +
+ + +
+ + 分数: +
diff --git a/apps/admin/src/views/question-store/modules/CategoryTree.vue b/apps/admin/src/views/question-store/modules/CategoryTree.vue index e81d101..dcfdf88 100644 --- a/apps/admin/src/views/question-store/modules/CategoryTree.vue +++ b/apps/admin/src/views/question-store/modules/CategoryTree.vue @@ -8,8 +8,9 @@ import { useDialog, useMessage, } from 'naive-ui' -import { ref, watch } from 'vue' +import { onMounted, ref, watch } from 'vue' import SvgIcon from '@/components/custom/svg-icon.vue' +import { fetchAddQuestion, fetchDeleteQuestion, fetchGetQuestionListAll, fetchUpdateQuestion } from '@/service/api/question' const emit = defineEmits<{ (e: 'update:category', node: any): void @@ -19,39 +20,19 @@ const message = useMessage() const dialog = useDialog() interface CategoryItem { - key: string - label: string + Id: number + Name: string + QuestionContent: string } // Mock Data - 模拟分类列表数据 -const categoryList = ref([ - { - key: 'stage-1', - label: '请根据提示书写正确的汉字。', - }, - { - key: 'stage-2', - label: '请写出含有“车”的汉字。', - }, - { - key: 'poem', - label: '请根据拼音书写正确的词语。', - }, - { - key: 'stage-extra-1', - label: '请写出含有反义字的四字成语。', - }, - { - key: 'stage-extra-2', - label: '请根据图片书写正确的成语。', - }, -]) +const categoryList = ref([]) -const selectedKey = ref(null) +const selectedKey = ref(null) // Modal State const showCategoryModal = ref(false) -const categoryForm = ref({ name: '' }) +const categoryForm = ref({ questionContent: '', name: '', Id: 0 }) const categoryOperation = ref<'add' | 'edit'>('add') const currentOperationNode = ref(null) @@ -60,12 +41,10 @@ watch(selectedKey, (newKey) => { emit('update:category', null) return } - const node = categoryList.value.find(item => item.key === newKey) + const node = categoryList.value.find(item => item.Id === newKey) if (node) { emit('update:category', { ...node, - level: 1, // Treat all as level 1 - isLeaf: true, // Treat all as leaf (container for questions) }) } else { @@ -73,12 +52,13 @@ watch(selectedKey, (newKey) => { } }) -function handleSelect(key: string) { +function handleSelect(key: number) { selectedKey.value = key } function handleAddCategory() { categoryOperation.value = 'add' + categoryForm.value.questionContent = '' categoryForm.value.name = '' currentOperationNode.value = null showCategoryModal.value = true @@ -86,7 +66,8 @@ function handleAddCategory() { function handleEditCategory(item: CategoryItem) { categoryOperation.value = 'edit' - categoryForm.value.name = item.label + categoryForm.value.name = item.Name + categoryForm.value.questionContent = item.QuestionContent currentOperationNode.value = item showCategoryModal.value = true } @@ -94,54 +75,98 @@ function handleEditCategory(item: CategoryItem) { function handleDeleteCategory(item: CategoryItem) { dialog.warning({ title: '警告', - content: `确定要删除分类 "${item.label}" 吗?此操作无法撤销。`, + content: `确定要删除分类 "${item.Name}" 吗?此操作无法撤销。`, positiveText: '确定', negativeText: '取消', - onPositiveClick: () => { - const index = categoryList.value.findIndex(n => n.key === item.key) + onPositiveClick: async () => { + const { error } = await fetchDeleteQuestion(item.Id) + if (error) { + message.error('删除分类失败') + return + } + const index = categoryList.value.findIndex(n => n.Id === item.Id) if (index !== -1) { categoryList.value.splice(index, 1) - if (selectedKey.value === item.key) { + if (selectedKey.value === item.Id) { selectedKey.value = null } + // 获取最新分类列表 + await fetchCategoryList() message.success('删除成功') } }, }) } -function submitCategory() { +/** + * 新增分类|更新分类 + * @param data 新增分类参数|更新分类参数 + */ +async function submitCategory() { if (!categoryForm.value.name) { message.error('请输入分类名称') return } if (categoryOperation.value === 'add') { - const newKey = `cate-${Date.now()}` - categoryList.value.push({ - key: newKey, - label: categoryForm.value.name, + const { error } = await fetchAddQuestion({ + questionContent: categoryForm.value.questionContent, + name: categoryForm.value.name, + id: 0, }) + if (error) { + message.error('添加分类失败') + return + } + await fetchCategoryList() message.success('分类添加成功') - // Automatically select the new category - selectedKey.value = newKey } else if (categoryOperation.value === 'edit' && currentOperationNode.value) { - currentOperationNode.value.label = categoryForm.value.name + const { error } = await fetchUpdateQuestion({ + questionContent: categoryForm.value.questionContent, + name: categoryForm.value.name, + id: currentOperationNode.value.Id, + }) + if (error) { + message.error('更新分类失败') + return + } + currentOperationNode.value.QuestionContent = categoryForm.value.questionContent message.success('分类修改成功') - // Trigger update if currently selected - if (selectedKey.value === currentOperationNode.value.key) { + await fetchCategoryList() + // 更新选中分类的信息 + if (selectedKey.value === currentOperationNode.value.Id) { const node = currentOperationNode.value emit('update:category', { ...node, - level: 1, - isLeaf: true, + Name: categoryForm.value.name, + QuestionContent: categoryForm.value.questionContent, }) } } showCategoryModal.value = false } + +/** + * 获取分类列表 + */ +async function fetchCategoryList() { + try { + const { data } = await fetchGetQuestionListAll() + // eslint-disable-next-line no-console + console.log('data', data) + categoryList.value = data?.data || [] + } + // eslint-disable-next-line unused-imports/no-unused-vars + catch (error) { + message.error('获取分类列表失败') + } +} + +onMounted(() => { + fetchCategoryList() +})