feat(字典管理): 新增字典管理功能及相关基础设施

- 新增字典管理页面,支持字典项的增删改查
- 添加 pinia-plugin-persistedstate 依赖,实现状态持久化
- 创建字典相关的 API 类型定义和服务接口
- 实现字典数据存储模块,支持字典数据的缓存和复用
- 添加 useDict 组合式函数,方便组件中使用字典数据
- 在题目分类和比赛创建模块中使用字典数据替代硬编码选项
- 优化分类树组件,添加拖拽手柄和题目类型选择
This commit is contained in:
2026-01-30 17:45:46 +08:00
parent b39761d58e
commit e1a19c48e0
21 changed files with 678 additions and 16 deletions

View File

@ -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))

View 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,
}
}

View File

@ -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: {

View File

@ -238,6 +238,7 @@ const local: App.I18n.Schema = {
'competition_competition-add': '比赛添加',
'competition_competition-detail': '比赛详情',
'competition_competition-list': '比赛列表',
'dictionary': '字典管理',
},
page: {
login: {

View File

@ -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"),

View File

@ -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',

View File

@ -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)?",

View 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 },
})
}

View File

@ -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)
}

View 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 })

View File

@ -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

View 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
}
}
}

View File

@ -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"

View File

@ -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,
})),
},
],

View File

@ -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
}

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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",

24
pnpm-lock.yaml generated
View File

@ -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