feat(字典管理): 新增字典管理功能及相关基础设施
- 新增字典管理页面,支持字典项的增删改查 - 添加 pinia-plugin-persistedstate 依赖,实现状态持久化 - 创建字典相关的 API 类型定义和服务接口 - 实现字典数据存储模块,支持字典数据的缓存和复用 - 添加 useDict 组合式函数,方便组件中使用字典数据 - 在题目分类和比赛创建模块中使用字典数据替代硬编码选项 - 优化分类树组件,添加拖拽手柄和题目类型选择
This commit is contained in:
@ -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))
|
||||
|
||||
|
||||
29
apps/admin/src/hooks/business/useDict.ts
Normal file
29
apps/admin/src/hooks/business/useDict.ts
Normal file
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
@ -238,6 +238,7 @@ const local: App.I18n.Schema = {
|
||||
'competition_competition-add': '比赛添加',
|
||||
'competition_competition-detail': '比赛详情',
|
||||
'competition_competition-list': '比赛列表',
|
||||
'dictionary': '字典管理',
|
||||
},
|
||||
page: {
|
||||
login: {
|
||||
|
||||
@ -23,6 +23,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
|
||||
"competition_competition-add": () => 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"),
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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)?",
|
||||
|
||||
59
apps/admin/src/service/api/dictionary.ts
Normal file
59
apps/admin/src/service/api/dictionary.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { request } from '../request'
|
||||
|
||||
/** Get dictionary list */
|
||||
export function getDictionaryList(roomID?: number) {
|
||||
return request<Api.Common.CommonResponse>({
|
||||
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<Api.Common.CommonResponse>({
|
||||
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 },
|
||||
})
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
70
apps/admin/src/store/modules/business/index.ts
Normal file
70
apps/admin/src/store/modules/business/index.ts
Normal file
@ -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<Api.Dictionary.DictionaryItem[]>([])
|
||||
const dictData = ref<Record<string, Api.Dictionary.DictionaryUIItem[]>>({})
|
||||
const isInitialized = ref(false)
|
||||
const loadingMap = ref<Record<string, boolean>>({})
|
||||
|
||||
/** 初始化字典 */
|
||||
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 })
|
||||
2
apps/admin/src/typings/api/common.d.ts
vendored
2
apps/admin/src/typings/api/common.d.ts
vendored
@ -24,7 +24,7 @@ declare namespace Api {
|
||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'currentPage' | 'pageSize'>
|
||||
|
||||
/** common response */
|
||||
interface CommonResponse {
|
||||
export interface CommonResponse {
|
||||
success: boolean
|
||||
code: number
|
||||
msg: string
|
||||
|
||||
37
apps/admin/src/typings/api/dictionary.d.ts
vendored
Normal file
37
apps/admin/src/typings/api/dictionary.d.ts
vendored
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
3
apps/admin/src/typings/elegant-router.d.ts
vendored
3
apps/admin/src/typings/elegant-router.d.ts
vendored
@ -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"
|
||||
|
||||
@ -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