feat(字典管理): 新增字典管理功能及相关基础设施
- 新增字典管理页面,支持字典项的增删改查 - 添加 pinia-plugin-persistedstate 依赖,实现状态持久化 - 创建字典相关的 API 类型定义和服务接口 - 实现字典数据存储模块,支持字典数据的缓存和复用 - 添加 useDict 组合式函数,方便组件中使用字典数据 - 在题目分类和比赛创建模块中使用字典数据替代硬编码选项 - 优化分类树组件,添加拖拽手柄和题目类型选择
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NModal, NStep, NSteps } from 'naive-ui'
|
||||
import { computed, onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { roundTypeOptions, scoreTypeOptions } from '@/constants/business'
|
||||
import { useDict } from '@/hooks/business/useDict'
|
||||
import { fetchCreateActivity, fetchCreateQuestion, fetchCreateTeamList, fetchGetRoomList } from '@/service/api/competition'
|
||||
/** 房间相关接口 */
|
||||
import BasicInfo from './modules/BasicInfo.vue'
|
||||
@ -19,6 +19,9 @@ const emit = defineEmits<{
|
||||
(e: 'success'): void
|
||||
}>()
|
||||
|
||||
const { options: roundTypeOptions } = useDict('round_type')
|
||||
const { options: scoreTypeOptions } = useDict('score_type')
|
||||
|
||||
const currentStep = ref<number>(1)
|
||||
/** 房间列表 */
|
||||
const roomListOptions = ref<{ label: string, value: number }[]>([])
|
||||
@ -42,13 +45,13 @@ const competitionData = ref({
|
||||
{
|
||||
id: 'round_default',
|
||||
name: '星·辞海遨游',
|
||||
roundType: roundTypeOptions[0].value,
|
||||
roundType: roundTypeOptions.value?.[0]?.value,
|
||||
questions: Array.from({ length: 10 }).map(() => ({
|
||||
value: null,
|
||||
score: 5,
|
||||
time: 30,
|
||||
title: '第1题',
|
||||
scoreType: scoreTypeOptions[0].value,
|
||||
scoreType: scoreTypeOptions.value?.[0]?.value,
|
||||
})),
|
||||
},
|
||||
],
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import { NButton, NForm, NFormItem, NInput, NInputNumber, NModal, NSelect, type SelectOption } from 'naive-ui'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { roundTypeOptions, scoreTypeOptions, timeOptions, uiTypeOptions } from '@/constants/business'
|
||||
import { useDict } from '@/hooks/business/useDict'
|
||||
import { fetchGetQuestionListAll } from '@/service/api/question'
|
||||
import { fetchTemplateList } from '@/service/api/template'
|
||||
import { useQuestionConfig } from './useQuestionConfig'
|
||||
@ -26,6 +26,12 @@ const props = defineProps<{
|
||||
}>
|
||||
}
|
||||
}>()
|
||||
|
||||
const { options: roundTypeOptions } = useDict('round_type')
|
||||
const { options: scoreTypeOptions } = useDict('score_type')
|
||||
const { options: timeOptions } = useDict('time_out')
|
||||
const { options: uiTypeOptions } = useDict('ui_type')
|
||||
|
||||
const {
|
||||
// getRoundScore,
|
||||
getRoundTime,
|
||||
@ -47,14 +53,14 @@ const showAddModal = ref(false)
|
||||
const newRoundForm = ref({
|
||||
name: '',
|
||||
count: 10,
|
||||
roundType: roundTypeOptions[0].value,
|
||||
roundType: roundTypeOptions?.value?.[0].value || '',
|
||||
})
|
||||
|
||||
function handleOpenAddModal() {
|
||||
newRoundForm.value = {
|
||||
name: `第${(props.modelValue?.rounds?.length || 0) + 1}轮 星·诗词大会`,
|
||||
count: 10,
|
||||
roundType: roundTypeOptions[0].value,
|
||||
roundType: roundTypeOptions?.value?.[0].value || '',
|
||||
}
|
||||
showAddModal.value = true
|
||||
}
|
||||
|
||||
218
apps/admin/src/views/dictionary/index.vue
Normal file
218
apps/admin/src/views/dictionary/index.vue
Normal file
@ -0,0 +1,218 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NDataTable, NForm, NFormItem, NInput, NModal, NPopconfirm, NSpace, useMessage } from 'naive-ui'
|
||||
import { h, onMounted, ref } from 'vue'
|
||||
import { addOrUpdateDictionary, deleteDictionary, getDictionaryList } from '@/service/api/dictionary'
|
||||
import DictionaryTypeModal from './modules/DictionaryTypeModal.vue'
|
||||
|
||||
// 表格数据
|
||||
const loading = ref(false)
|
||||
const tableData = ref<Api.Dictionary.DictionaryItem[]>([])
|
||||
|
||||
// 字典类型模态框
|
||||
const showTypeModal = ref(false)
|
||||
const currentDictionaryId = ref(0)
|
||||
|
||||
function handleOpenTypeModal(row: Api.Dictionary.DictionaryItem) {
|
||||
currentDictionaryId.value = row.Id
|
||||
showTypeModal.value = true
|
||||
}
|
||||
|
||||
// 模态框控制
|
||||
const showModal = ref(false)
|
||||
const modalTitle = ref('新增字典')
|
||||
const formRef = ref()
|
||||
const formData = ref<Api.Dictionary.AddOrUpdateParams>({
|
||||
Id: 0,
|
||||
DicKey: '',
|
||||
DicValue: '',
|
||||
bakValue: '',
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
dicKey: { required: true, message: '请输入字典键', trigger: 'blur' },
|
||||
dicValue: { required: true, message: '请输入字典值', trigger: 'blur' },
|
||||
}
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
// 获取列表数据
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const { data: res, error } = await getDictionaryList()
|
||||
if (!error && res) {
|
||||
tableData.value = res.data || []
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 列定义
|
||||
const columns: DataTableColumns<Api.Dictionary.DictionaryItem> = [
|
||||
{ title: 'ID', key: 'Id' },
|
||||
{
|
||||
title: '字典键 (Key)',
|
||||
key: 'DicKey',
|
||||
render(row) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'primary',
|
||||
onClick: () => handleOpenTypeModal(row),
|
||||
},
|
||||
{ default: () => row.DicKey },
|
||||
)
|
||||
},
|
||||
},
|
||||
{ title: '字典值 (Value)', key: 'DicValue' },
|
||||
{ title: '备注/备份值', key: 'BakValue' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
return h(NSpace, null, {
|
||||
default: () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => handleEdit(row),
|
||||
},
|
||||
{ default: () => '编辑' },
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(row),
|
||||
},
|
||||
{
|
||||
default: () => '确认删除该条数据吗?',
|
||||
trigger: () => h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
},
|
||||
{ default: () => '删除' },
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
// 新增
|
||||
function handleAdd() {
|
||||
modalTitle.value = '新增字典'
|
||||
formData.value = {
|
||||
Id: 0,
|
||||
DicKey: '',
|
||||
DicValue: '',
|
||||
bakValue: '',
|
||||
}
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
function handleEdit(row: Api.Dictionary.DictionaryItem) {
|
||||
modalTitle.value = '编辑字典'
|
||||
formData.value = { ...row }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
async function handleDelete(row: Api.Dictionary.DictionaryItem) {
|
||||
try {
|
||||
await deleteDictionary(row.Id)
|
||||
message.success('删除成功')
|
||||
fetchData()
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function handleSubmit() {
|
||||
formRef.value?.validate(async (errors: any) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
await addOrUpdateDictionary(formData.value)
|
||||
message.success(formData.value.Id ? '更新成功' : '新增成功')
|
||||
showModal.value = false
|
||||
fetchData()
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full p-4">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- 这里可以放搜索框等 -->
|
||||
</div>
|
||||
<NButton type="primary" @click="handleAdd">
|
||||
新增字典
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<NDataTable
|
||||
:columns="columns" :data="tableData" :loading="loading" :pagination="{ pageSize: 10 }"
|
||||
:row-key="(row) => row.id" bordered class="h-[calc(100%-3rem)]"
|
||||
/>
|
||||
|
||||
<DictionaryTypeModal
|
||||
v-model:visible="showTypeModal"
|
||||
:dictionary-id="currentDictionaryId"
|
||||
/>
|
||||
|
||||
<NModal v-model:show="showModal" :title="modalTitle" preset="card" class="w-[600px]">
|
||||
<NForm
|
||||
ref="formRef" :model="formData" :rules="rules" label-placement="left" label-width="100"
|
||||
require-mark-placement="right-hanging"
|
||||
>
|
||||
<NFormItem label="字典名称" path="DicKey">
|
||||
<NInput v-model:value="formData.DicKey" placeholder="请输入字典名称" />
|
||||
</NFormItem>
|
||||
<NFormItem label="字典类型" path="DicValue">
|
||||
<NInput v-model:value="formData.DicValue" placeholder="请输入字典值" type="textarea" />
|
||||
</NFormItem>
|
||||
<NFormItem label="备注" path="bakValue">
|
||||
<NInput v-model:value="formData.bakValue" placeholder="请输入备注或备份值" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<NSpace justify="end">
|
||||
<NButton @click="showModal = false">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">
|
||||
确定
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
176
apps/admin/src/views/dictionary/modules/DictionaryTypeModal.vue
Normal file
176
apps/admin/src/views/dictionary/modules/DictionaryTypeModal.vue
Normal file
@ -0,0 +1,176 @@
|
||||
<script setup lang="ts">
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NDataTable, NForm, NFormItem, NInput, NModal, NPopconfirm, NSpace, useMessage } from 'naive-ui'
|
||||
import { h, ref, watch } from 'vue'
|
||||
import { addOrUpdateDictionaryUIType, deleteDictionaryItem, getDictionaryListUITypeByDicID } from '@/service/api/dictionary'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
dictionaryId: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const message = useMessage()
|
||||
|
||||
// Modal control for Add/Edit UI Type
|
||||
const showEditModal = ref(false)
|
||||
const modalTitle = ref('新增字典类型')
|
||||
const formRef = ref()
|
||||
const formData = ref<Api.Dictionary.AddOrUpdateDictionaryUITypeParams>({
|
||||
id: 0,
|
||||
uI_Key: '',
|
||||
uI_Value: '',
|
||||
bakValue: '',
|
||||
dictionaryID: 0,
|
||||
})
|
||||
|
||||
const rules = {
|
||||
uI_Key: { required: true, message: '请输入键', trigger: 'blur' },
|
||||
uI_Value: { required: true, message: '请输入值', trigger: 'blur' },
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
if (!props.dictionaryId)
|
||||
return
|
||||
loading.value = true
|
||||
try {
|
||||
const { data: res, error } = await getDictionaryListUITypeByDicID(props.dictionaryId)
|
||||
if (!error && res) {
|
||||
tableData.value = res.data || []
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
fetchData()
|
||||
}
|
||||
})
|
||||
|
||||
const columns: DataTableColumns<any> = [
|
||||
{ title: 'ID', key: 'Id', width: 80 },
|
||||
{ title: '键 (Key)', key: 'UI_Key' },
|
||||
{ title: '值 (Value)', key: 'UI_Value' },
|
||||
{ title: '备注', key: 'BakValue' },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
render(row) {
|
||||
return h(NSpace, null, {
|
||||
default: () => [
|
||||
h(NButton, { size: 'small', type: 'primary', onClick: () => handleEdit(row) }, { default: () => '编辑' }),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => handleDelete(row) },
|
||||
{
|
||||
default: () => '确认删除?',
|
||||
trigger: () => h(NButton, { size: 'small', type: 'error' }, { default: () => '删除' }),
|
||||
},
|
||||
),
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function handleAdd() {
|
||||
modalTitle.value = '新增字典类型'
|
||||
formData.value = {
|
||||
id: 0,
|
||||
uI_Key: '',
|
||||
uI_Value: '',
|
||||
bakValue: '',
|
||||
dictionaryID: props.dictionaryId,
|
||||
}
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row: any) {
|
||||
modalTitle.value = '编辑字典类型'
|
||||
const { BakValue, ID, UI_Key, UI_Value } = row
|
||||
formData.value = { ...row, bakValue: BakValue, id: ID, uI_Key: UI_Key, uI_Value: UI_Value, dictionaryID: props.dictionaryId }
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
const { error } = await deleteDictionaryItem(row.ID)
|
||||
if (!error) {
|
||||
message.success('删除成功')
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
formRef.value?.validate(async (errors: any) => {
|
||||
if (!errors) {
|
||||
try {
|
||||
const { error } = await addOrUpdateDictionaryUIType(formData.value)
|
||||
if (!error) {
|
||||
message.success('操作成功')
|
||||
showEditModal.value = false
|
||||
fetchData()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal :show="visible" preset="card" title="字典类型管理" class="w-[800px]" @update:show="handleClose">
|
||||
<div class="mb-4">
|
||||
<NButton type="primary" @click="handleAdd">
|
||||
新增类型
|
||||
</NButton>
|
||||
</div>
|
||||
<NDataTable :columns="columns" :data="tableData" :loading="loading" bordered :row-key="(row) => row.id" />
|
||||
|
||||
<NModal v-model:show="showEditModal" :title="modalTitle" preset="card" class="w-[500px]">
|
||||
<NForm ref="formRef" :model="formData" :rules="rules" label-placement="left" label-width="80">
|
||||
<NFormItem label="键" path="uI_Key">
|
||||
<NInput v-model:value="formData.uI_Key" placeholder="请输入键" />
|
||||
</NFormItem>
|
||||
<NFormItem label="值" path="uI_Value">
|
||||
<NInput v-model:value="formData.uI_Value" placeholder="请输入值" />
|
||||
</NFormItem>
|
||||
<NFormItem label="备注" path="bakValue">
|
||||
<NInput v-model:value="formData.bakValue" placeholder="请输入备注" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<NSpace justify="end">
|
||||
<NButton @click="showEditModal = false">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">
|
||||
确定
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NModal>
|
||||
</NModal>
|
||||
</template>
|
||||
@ -50,10 +50,10 @@ function onMouseUp() {
|
||||
<div class="h-full flex overflow-hidden rounded-2xl bg-white shadow-sm">
|
||||
<!-- 分类树区域 -->
|
||||
<div class="relative h-full flex-shrink-0" :style="{ width: `${siderWidth}px` }">
|
||||
<CategoryTree @update:category="val => currentCategory = val" />
|
||||
<CategoryTree @update:category="val => currentCategory = val" @drag-start="onMouseDown" />
|
||||
<!-- 分类树宽度调整手柄 -->
|
||||
<div
|
||||
class="absolute right-0 top-0 z-10 h-full w-1 cursor-col-resize transition-colors active:bg-primary/40 hover:bg-primary/20"
|
||||
class="drag-handle absolute right-0 top-0 z-10 h-full w-1 cursor-col-resize transition-colors active:bg-primary/40 hover:bg-primary/20"
|
||||
@mousedown="onMouseDown"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -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<CategoryItem[]>([])
|
||||
|
||||
const selectedKey = ref<number | null>(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<CategoryItem | null>(null)
|
||||
|
||||
@ -170,7 +173,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col border-r border-gray-100 bg-gray-50/30">
|
||||
<div class="drag-handle relative h-full flex flex-col border-r border-gray-100 bg-gray-50/30">
|
||||
<div class="flex items-center justify-between border-b border-gray-100 px-4 py-4">
|
||||
<span class="text-lg text-gray-700 font-bold">题目分类</span>
|
||||
<NButton size="tiny" secondary type="primary" @click="handleAddCategory">
|
||||
@ -227,9 +230,15 @@ onMounted(() => {
|
||||
class="w-[500px]"
|
||||
>
|
||||
<NForm>
|
||||
<!-- 分类显示名称 -->
|
||||
<NFormItem label="分类名称">
|
||||
<NInput v-model:value="categoryForm.name" placeholder="请输入分类名称" @keyup.enter="submitCategory" />
|
||||
</NFormItem>
|
||||
<!-- 题目类型 -->
|
||||
<NFormItem label="题目类型">
|
||||
<NSelect v-model:value="categoryForm.questionType" :options="questionTypeOptions" placeholder="请选择题目类型" />
|
||||
</NFormItem>
|
||||
<!-- 分类描述 -->
|
||||
<NFormItem label="分类描述">
|
||||
<NInput v-model:value="categoryForm.questionContent" placeholder="请输入分类描述" @keyup.enter="submitCategory" />
|
||||
</NFormItem>
|
||||
@ -245,9 +254,13 @@ onMounted(() => {
|
||||
</div>
|
||||
</template>
|
||||
</NModal>
|
||||
|
||||
<!-- 拖动把手图标 -->
|
||||
<div
|
||||
class="absolute right-0 top-1/2 z-50 h-8 w-4 flex translate-x-1/2 cursor-col-resize items-center justify-center border border-gray-200 rounded-full bg-white shadow-sm transition-all -translate-y-1/2 hover:scale-110 hover:bg-gray-50"
|
||||
@mousedown="(e) => emit('dragStart', e)"
|
||||
>
|
||||
<SvgIcon icon="carbon:draggable" class="text-[10px] text-gray-400" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* No specific styles needed as we use Tailwind classes */
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user