feat(admin): 更新题库管理和环境配置
- 禁用强制登录以便本地开发 - 更新开发环境API基础URL为本地服务 - 在代理配置中添加secure: false以支持HTTPS接口 - 修复请求响应中字符串类型数据的JSON解析问题 - 新增题目类型相关常量定义和类型声明 - 扩展transformRecordToOption函数支持数字键值 - 更新题库相关API接口和类型定义 - 在竞赛配置中添加题目分数字段 - 重构题库管理界面,实现分类和题目的完整CRUD操作 - 删除未使用的模板管理组件
This commit is contained in:
@ -63,6 +63,13 @@ export const timeRecord: Record<Api.Competition.QuestionTimeType, string> = {
|
||||
90: '90s',
|
||||
}
|
||||
|
||||
export const questionTypeRecord: Record<number, string> = {
|
||||
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[]
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
import { request } from '../request'
|
||||
|
||||
/** 获取所有题目列表 */
|
||||
export function fetchGetQuestionListAll(params?: any) {
|
||||
return request<Api.Question.CommonRecord[]>({
|
||||
export function fetchGetQuestionListAll() {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: '/Base/ActivityMain/GetQuestionListAll',
|
||||
method: 'get',
|
||||
params: params || {},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** 新增题目 */
|
||||
export function fetchAddQuestion(data?: Api.Question.AddParams) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
@ -19,3 +16,86 @@ export function fetchAddQuestion(data?: Api.Question.AddParams) {
|
||||
data: data || {},
|
||||
})
|
||||
}
|
||||
|
||||
/** 更新题目 */
|
||||
export function fetchUpdateQuestion(data?: Api.Question.AddParams) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: '/Base/ActivityMain/UpdateQuestionList',
|
||||
method: 'post',
|
||||
data: data || {},
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除题目 */
|
||||
export function fetchDeleteQuestion(id: number) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
url: `/Base/ActivityMain/DeleteQuestionList/?Id=${id}`,
|
||||
method: 'post',
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取所有问题库列表(分页) */
|
||||
export function fetchGetQuestionLibraryListAll(params: Api.Question.GetQuestionLibraryListAllParams) {
|
||||
return request<Api.Question.CommonRecord>({
|
||||
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<Api.Question.CommonRecord>({
|
||||
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<Api.Question.CommonRecord>({
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -28,7 +28,12 @@ export const request = createFlatRequest(
|
||||
} as RequestInstanceState,
|
||||
// 响应数据转换
|
||||
transform(response: AxiosResponse<App.Service.Response<any>>) {
|
||||
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) {
|
||||
|
||||
40
apps/admin/src/typings/api/question.d.ts
vendored
40
apps/admin/src/typings/api/question.d.ts
vendored
@ -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<T = any> 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<Common.PaginatingCommonParams, 'current' | 'size'>
|
||||
|
||||
|
||||
2
apps/admin/src/typings/components.d.ts
vendored
2
apps/admin/src/typings/components.d.ts
vendored
@ -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']
|
||||
|
||||
@ -17,10 +17,11 @@ import { $t } from '@/locales'
|
||||
* ```;
|
||||
*
|
||||
* @param record
|
||||
* @param isNumberKey
|
||||
*/
|
||||
export function transformRecordToOption<T extends Record<string, string>>(record: T) {
|
||||
export function transformRecordToOption<T extends Record<string, string>>(record: T, isNumberKey = false) {
|
||||
return Object.entries(record).map(([value, label]) => ({
|
||||
value,
|
||||
value: isNumberKey ? Number(value) : value,
|
||||
label,
|
||||
})) as CommonType.Option<keyof T, T[keyof T]>[]
|
||||
}
|
||||
@ -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<v2 | 0 v1==v2
|
||||
*/
|
||||
@ -76,12 +77,14 @@ export function compareVersion(v1: string, v2: string, separator = '.') {
|
||||
const s1 = v1.split(separator)
|
||||
const s2 = v2.split(separator)
|
||||
const len = Math.max(s1.length, s2.length)
|
||||
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const num1 = parseInt(s1[i] || '0')
|
||||
const num2 = parseInt(s2[i] || '0')
|
||||
if (num1 > 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
|
||||
}
|
||||
|
||||
@ -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 })
|
||||
</div>
|
||||
|
||||
<!-- 时间 -->
|
||||
<div class="col-span-6 flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-1.5">
|
||||
<div class="col-span-4 flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-1.5">
|
||||
<icon-ic-outline-timer class="text-gray-400" />
|
||||
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">答题时间:</span>
|
||||
<NSelect
|
||||
@ -303,13 +304,23 @@ defineExpose({ validate, reset })
|
||||
</div>
|
||||
|
||||
<!-- 分数规则 -->
|
||||
<div class="col-span-6 flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-1.5">
|
||||
<div class="col-span-4 flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-1.5">
|
||||
<icon-streamline-sharp:type-area-remix class="text-gray-400" />
|
||||
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">分数规则:</span>
|
||||
<NSelect
|
||||
v-model:value="item.scoreType" :options="scoreTypeOptions as unknown as SelectOption[]"
|
||||
class="flex-1 !bg-transparent" size="small" :bordered="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 分数 -->
|
||||
<div class="col-span-4 flex items-center gap-2 rounded-lg bg-gray-50 px-3 py-1.5">
|
||||
<icon-ic-round-star-border class="text-gray-400" />
|
||||
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">分数:</span>
|
||||
<NInputNumber
|
||||
v-model:value="item.score" :min="0" :show-button="false" class="flex-1 !bg-transparent"
|
||||
size="small" :bordered="false" placeholder="0"
|
||||
/>
|
||||
<span class="text-xs text-gray-400 font-bold">分</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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<CategoryItem[]>([
|
||||
{
|
||||
key: 'stage-1',
|
||||
label: '请根据提示书写正确的汉字。',
|
||||
},
|
||||
{
|
||||
key: 'stage-2',
|
||||
label: '请写出含有“车”的汉字。',
|
||||
},
|
||||
{
|
||||
key: 'poem',
|
||||
label: '请根据拼音书写正确的词语。',
|
||||
},
|
||||
{
|
||||
key: 'stage-extra-1',
|
||||
label: '请写出含有反义字的四字成语。',
|
||||
},
|
||||
{
|
||||
key: 'stage-extra-2',
|
||||
label: '请根据图片书写正确的成语。',
|
||||
},
|
||||
])
|
||||
const categoryList = ref<CategoryItem[]>([])
|
||||
|
||||
const selectedKey = ref<string | null>(null)
|
||||
const selectedKey = ref<number | null>(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<CategoryItem | null>(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()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -161,17 +186,17 @@ function submitCategory() {
|
||||
暂无分类,请点击上方添加
|
||||
</div>
|
||||
<div
|
||||
v-for="item in categoryList" :key="item.key"
|
||||
v-for="item in categoryList" :key="item.Id"
|
||||
class="group mb-1 flex cursor-pointer items-center justify-between rounded-lg px-3 py-2.5 transition-all hover:bg-white hover:shadow-sm"
|
||||
:class="selectedKey === item.key ? 'bg-white shadow-sm text-primary' : 'text-gray-600'"
|
||||
@click="handleSelect(item.key)"
|
||||
:class="selectedKey === item.Id ? 'bg-white shadow-sm text-primary' : 'text-gray-600'"
|
||||
@click="handleSelect(item.Id)"
|
||||
>
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<SvgIcon
|
||||
icon="carbon:folder" class="flex-shrink-0 text-lg"
|
||||
:class="selectedKey === item.key ? 'text-primary' : 'text-gray-400'"
|
||||
:class="selectedKey === item.Id ? 'text-primary' : 'text-gray-400'"
|
||||
/>
|
||||
<span class="truncate font-medium">{{ item.label }}</span>
|
||||
<span class="truncate font-medium">{{ item.Name }} - {{ item.QuestionContent }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
@ -205,6 +230,9 @@ function submitCategory() {
|
||||
<NFormItem label="分类名称">
|
||||
<NInput v-model:value="categoryForm.name" placeholder="请输入分类名称" @keyup.enter="submitCategory" />
|
||||
</NFormItem>
|
||||
<NFormItem label="分类描述">
|
||||
<NInput v-model:value="categoryForm.questionContent" placeholder="请输入分类描述" @keyup.enter="submitCategory" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-3">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInst, UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui'
|
||||
import {
|
||||
NBreadcrumb,
|
||||
NBreadcrumbItem,
|
||||
@ -9,181 +10,241 @@ import {
|
||||
NEmpty,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NImage,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NPagination,
|
||||
NRadio,
|
||||
NRadioGroup,
|
||||
NSelect,
|
||||
NTag,
|
||||
|
||||
NUpload,
|
||||
useDialog,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue'
|
||||
import { questionTypeOptions } from '@/constants/business'
|
||||
import { fetchAddQuestionLibrary, fetchDeleteQuestionLibrary, fetchGetQuestionLibraryListAll, fetchUpdateQuestionLibrary } from '@/service/api/question'
|
||||
|
||||
const props = defineProps<{
|
||||
currentCategory: any
|
||||
}>()
|
||||
|
||||
// Mock Data - 模拟题目列表数据
|
||||
const questionList = ref([
|
||||
{
|
||||
id: 'Q-0325',
|
||||
categoryKey: 'stage-1',
|
||||
content: 'jiū 表示小鸟的叫声',
|
||||
answer: '碧',
|
||||
score: 10,
|
||||
time: 30,
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
// 题目列表数据
|
||||
const questionList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
onChange: (page: number) => {
|
||||
pagination.value.page = page
|
||||
if (props.currentCategory?.Id) {
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Q-0326',
|
||||
categoryKey: 'stage-1',
|
||||
content: '“春色满园关不住,一枝红杏出墙来”。请书写“杏”字。',
|
||||
answer: '杏',
|
||||
score: 10,
|
||||
time: 30,
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
if (props.currentCategory?.Id) {
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Q-0327',
|
||||
categoryKey: 'stage-1',
|
||||
content: '“春色满园关不住,一枝红杏出墙来”。请书写“㽱”字。',
|
||||
answer: '㽱',
|
||||
score: 10,
|
||||
time: 30,
|
||||
},
|
||||
{
|
||||
id: 'Q-0328',
|
||||
categoryKey: 'stage-1',
|
||||
content: '“春色满园关不住,一枝红杏出墙来”。请书写“不”字。',
|
||||
answer: '不',
|
||||
score: 10,
|
||||
time: 30,
|
||||
},
|
||||
{
|
||||
id: 'Q-0329',
|
||||
categoryKey: 'q-type-2',
|
||||
content: 'táng',
|
||||
answer: '糖',
|
||||
score: 10,
|
||||
time: 30,
|
||||
},
|
||||
{
|
||||
id: 'Q-0330',
|
||||
categoryKey: 'q-type-2',
|
||||
content: 'lái',
|
||||
answer: '莱',
|
||||
score: 10,
|
||||
time: 30,
|
||||
},
|
||||
{
|
||||
id: 'Q-0331',
|
||||
categoryKey: 'q-type-3',
|
||||
content: '请写出含有“木”字的汉字。',
|
||||
answer: '林',
|
||||
score: 10,
|
||||
time: 30,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
const searchText = ref('')
|
||||
const showQuestionModal = ref(false)
|
||||
const questionOperation = ref<'add' | 'edit'>('add')
|
||||
const currentQuestionId = ref<string | null>(null)
|
||||
const currentQuestionId = ref<number | null>(null)
|
||||
const questionForm = ref({
|
||||
content: '',
|
||||
name: '',
|
||||
answer: '',
|
||||
score: 10,
|
||||
time: 30,
|
||||
type: 0,
|
||||
imageUrl: '',
|
||||
IsGood: 0,
|
||||
})
|
||||
|
||||
const isLeafSelected = computed(() => {
|
||||
const fileList = ref<UploadFileInfo[]>([])
|
||||
|
||||
const questionFormRef = ref<FormInst | null>(null)
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入题目正文内容', trigger: ['blur'] }],
|
||||
answer: [{ required: true, message: '请输入题目答案', trigger: ['blur'] }],
|
||||
type: [{ required: true, message: '请选择题目类型', trigger: ['change'], type: 'number' as const }],
|
||||
}
|
||||
|
||||
const hasCategory = computed(() => {
|
||||
return !!props.currentCategory
|
||||
})
|
||||
|
||||
const filteredQuestions = computed(() => {
|
||||
let list = questionList.value
|
||||
|
||||
// 1. Filter by Category Key
|
||||
if (props.currentCategory && props.currentCategory.key) {
|
||||
list = list.filter(q => q.categoryKey === props.currentCategory.key)
|
||||
watch(() => props.currentCategory, (newVal) => {
|
||||
if (newVal && newVal.Id) {
|
||||
pagination.value.page = 1
|
||||
searchText.value = ''
|
||||
fetchData(newVal.Id)
|
||||
}
|
||||
|
||||
// 2. Filter by Search Text
|
||||
if (searchText.value) {
|
||||
list = list.filter(q => q.content.includes(searchText.value))
|
||||
else {
|
||||
questionList.value = []
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
return list
|
||||
})
|
||||
function customRequest({ onFinish }: UploadCustomRequestOptions) {
|
||||
onFinish()
|
||||
}
|
||||
|
||||
function handleUploadChange(data: { fileList: UploadFileInfo[] }) {
|
||||
fileList.value = data.fileList
|
||||
}
|
||||
|
||||
async function fetchData(id: number) {
|
||||
loading.value = true
|
||||
const { data, error } = await fetchGetQuestionLibraryListAll({
|
||||
questionId: id,
|
||||
pageIndex: pagination.value.page,
|
||||
pageSizes: pagination.value.pageSize,
|
||||
keyWords: searchText.value,
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(data, 'data')
|
||||
if (!error) {
|
||||
questionList.value = data?.data?.QuestionListDetailList || []
|
||||
pagination.value.itemCount = data?.data?.TotalNum || 0
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.value.page = 1
|
||||
if (props.currentCategory?.Id) {
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddQuestion() {
|
||||
if (!props.currentCategory) {
|
||||
window.$message?.warning('请先选择一个分类')
|
||||
message.warning('请先选择一个分类')
|
||||
return
|
||||
}
|
||||
questionOperation.value = 'add'
|
||||
currentQuestionId.value = null
|
||||
questionForm.value = { content: '', answer: '', score: 10, time: 30, imageUrl: '' }
|
||||
questionForm.value = { name: '', answer: '', type: 0, imageUrl: '', IsGood: 0 }
|
||||
fileList.value = []
|
||||
showQuestionModal.value = true
|
||||
}
|
||||
|
||||
function handleEditQuestion(id: string) {
|
||||
const question = questionList.value.find(q => q.id === id)
|
||||
function handleEditQuestion(id: number) {
|
||||
const question = questionList.value.find(q => q.Id === id)
|
||||
if (question) {
|
||||
questionOperation.value = 'edit'
|
||||
currentQuestionId.value = id
|
||||
questionForm.value = {
|
||||
content: question.content,
|
||||
answer: question.answer,
|
||||
score: question.score,
|
||||
time: question.time,
|
||||
imageUrl: (question as any).imageUrl || '',
|
||||
name: question.Name,
|
||||
answer: question.Answer,
|
||||
type: question.Type,
|
||||
imageUrl: question.ImageUrl || '',
|
||||
IsGood: question.IsPriority || 0,
|
||||
}
|
||||
fileList.value = []
|
||||
if (question.ImageUrl) {
|
||||
fileList.value = [{
|
||||
id: 'existing',
|
||||
name: 'Existing Image',
|
||||
status: 'finished',
|
||||
url: question.ImageUrl,
|
||||
}]
|
||||
}
|
||||
showQuestionModal.value = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteQuestion(id: string) {
|
||||
window.$dialog?.warning({
|
||||
function handleDeleteQuestion(id: number) {
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这道题目吗?此操作无法撤销。',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
const index = questionList.value.findIndex(q => q.id === id)
|
||||
if (index !== -1) {
|
||||
questionList.value.splice(index, 1)
|
||||
window.$message?.success('删除成功')
|
||||
onPositiveClick: async () => {
|
||||
const { error } = await fetchDeleteQuestionLibrary([id])
|
||||
if (!error) {
|
||||
message.success('删除成功')
|
||||
if (props.currentCategory?.Id) {
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
}
|
||||
else {
|
||||
message.error('删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function submitQuestion() {
|
||||
if (!questionForm.value.content || !questionForm.value.answer) {
|
||||
window.$message?.error('请填写完整信息')
|
||||
async function submitQuestion() {
|
||||
const valid = await questionFormRef.value?.validate()
|
||||
|
||||
if (!valid) {
|
||||
message.error('请填写完整信息')
|
||||
return
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('questionForm.value', questionForm.value)
|
||||
|
||||
if (questionOperation.value === 'add') {
|
||||
// Mock add
|
||||
questionList.value.push({
|
||||
id: `Q-${Math.floor(Math.random() * 10000)}`,
|
||||
...questionForm.value,
|
||||
categoryKey: props.currentCategory?.key, // Add categoryKey
|
||||
})
|
||||
window.$message?.success('题目添加成功')
|
||||
const commonParams = {
|
||||
name: questionForm.value.name,
|
||||
answer: questionForm.value.answer,
|
||||
imageUrl: '',
|
||||
type: questionForm.value.type as any,
|
||||
questionId: props.currentCategory.Id,
|
||||
IsGood: questionForm.value.IsGood,
|
||||
}
|
||||
else if (questionOperation.value === 'edit' && currentQuestionId.value) {
|
||||
const index = questionList.value.findIndex(q => q.id === currentQuestionId.value)
|
||||
if (index !== -1) {
|
||||
questionList.value[index] = {
|
||||
...questionList.value[index],
|
||||
...questionForm.value,
|
||||
}
|
||||
window.$message?.success('题目修改成功')
|
||||
|
||||
let fileToUpload: File | null = null
|
||||
if (questionForm.value.type === 1) { // 图片类型 (1: 图片, 0: 文字 - based on previous fixes)
|
||||
if (fileList.value.length > 0 && fileList.value[0].file) {
|
||||
fileToUpload = fileList.value[0].file
|
||||
}
|
||||
else if (questionOperation.value === 'add' && fileList.value.length === 0) {
|
||||
message.error('请上传图片')
|
||||
return
|
||||
}
|
||||
else if (questionOperation.value === 'edit' && fileList.value.length === 0) {
|
||||
message.error('请上传图片')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (questionOperation.value === 'add') {
|
||||
const { error } = await fetchAddQuestionLibrary({
|
||||
...commonParams,
|
||||
id: 0,
|
||||
}, fileToUpload)
|
||||
if (!error) {
|
||||
message.success('题目添加成功')
|
||||
showQuestionModal.value = false
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
else {
|
||||
message.error('添加失败')
|
||||
}
|
||||
}
|
||||
else if (questionOperation.value === 'edit' && currentQuestionId.value) {
|
||||
const { error } = await fetchUpdateQuestionLibrary({
|
||||
...commonParams,
|
||||
id: currentQuestionId.value,
|
||||
}, fileToUpload)
|
||||
if (!error) {
|
||||
message.success('题目修改成功')
|
||||
showQuestionModal.value = false
|
||||
fetchData(props.currentCategory.Id)
|
||||
}
|
||||
else {
|
||||
message.error('修改失败')
|
||||
}
|
||||
}
|
||||
showQuestionModal.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -194,20 +255,20 @@ function submitQuestion() {
|
||||
<NBreadcrumb>
|
||||
<NBreadcrumbItem>题库全集</NBreadcrumbItem>
|
||||
<NBreadcrumbItem v-if="currentCategory && currentCategory.level === 1">
|
||||
{{ currentCategory.label }}
|
||||
{{ currentCategory.name }}
|
||||
</NBreadcrumbItem>
|
||||
</NBreadcrumb>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="m-0 text-xl text-gray-800 font-bold">
|
||||
{{ currentCategory ? currentCategory.label : '' }}
|
||||
{{ currentCategory ? currentCategory.name : '' }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div v-if="currentCategory" class="mt-2 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<NInput v-model:value="searchText" placeholder="输入题目关键字在当前分类下搜索..." class="!w-80">
|
||||
<NInput v-model:value="searchText" placeholder="输入题目关键字在当前分类下搜索..." class="!w-80" @keyup.enter="handleSearch">
|
||||
<template #prefix>
|
||||
<SvgIcon icon="carbon:search" class="text-gray-400" />
|
||||
</template>
|
||||
@ -225,7 +286,7 @@ function submitQuestion() {
|
||||
|
||||
<!-- 题目列表区域 -->
|
||||
<div class="flex-1 overflow-y-auto bg-gray-50/50 p-6">
|
||||
<template v-if="!isLeafSelected">
|
||||
<template v-if="!hasCategory">
|
||||
<div class="h-full flex flex-col items-center justify-center text-gray-400">
|
||||
<NEmpty description="暂无数据">
|
||||
<template #extra>
|
||||
@ -235,7 +296,7 @@ function submitQuestion() {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="filteredQuestions.length === 0">
|
||||
<template v-else-if="questionList.length === 0">
|
||||
<div class="mt-20 flex justify-center">
|
||||
<NEmpty description="该分类下暂无题目" />
|
||||
</div>
|
||||
@ -244,20 +305,28 @@ function submitQuestion() {
|
||||
<!-- 题目列表 -->
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<NCard v-for="q in filteredQuestions" :key="q.id" size="small" hoverable class="rounded-xl">
|
||||
<NCard v-for="q in questionList" :key="q.Id" size="small" hoverable class="rounded-xl">
|
||||
<template #header>
|
||||
<div class="flex items-center gap-2">
|
||||
<NTag size="small" type="primary" :bordered="false">
|
||||
ID: {{ q.id }}
|
||||
ID: {{ q.Id }}
|
||||
</NTag>
|
||||
<NTag size="small" :type="q.IsGood === 1 ? 'info' : 'default'" :bordered="false">
|
||||
{{ q.IsGood === 1 ? '优先' : '随机' }}
|
||||
</NTag>
|
||||
</div>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<span class="text-xs text-gray-400 font-mono">{{ q.score }} PTS / {{ q.time }} S</span>
|
||||
<!-- <span class="text-xs text-gray-400 font-mono">{{ q.score }} PTS / {{ q.time }} S</span> -->
|
||||
</template>
|
||||
|
||||
<div class="py-2 text-base text-gray-700 font-medium">
|
||||
{{ q.content }}
|
||||
{{ q.Name }}
|
||||
</div>
|
||||
|
||||
<!-- 图片展示 -->
|
||||
<div v-if="q.ImageUrl" class="mb-2">
|
||||
<NImage :src="q.ImageUrl" width="100" class="block rounded-lg" />
|
||||
</div>
|
||||
|
||||
<!-- 标准答案 -->
|
||||
@ -266,19 +335,19 @@ function submitQuestion() {
|
||||
STANDARD ANSWER
|
||||
</div>
|
||||
<div class="text-green-800 font-bold">
|
||||
{{ q.answer }}
|
||||
{{ q.Answer }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<template #action>
|
||||
<div class="flex justify-end gap-2">
|
||||
<NButton size="tiny" quaternary type="primary" @click="handleEditQuestion(q.id)">
|
||||
<NButton size="tiny" quaternary type="primary" @click="handleEditQuestion(q.Id)">
|
||||
<template #icon>
|
||||
<SvgIcon icon="carbon:edit" />
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton size="tiny" quaternary type="error" @click="handleDeleteQuestion(q.id)">
|
||||
<NButton size="tiny" quaternary type="error" @click="handleDeleteQuestion(q.Id)">
|
||||
<template #icon>
|
||||
<SvgIcon icon="carbon:trash-can" />
|
||||
</template>
|
||||
@ -287,35 +356,68 @@ function submitQuestion() {
|
||||
</template>
|
||||
</NCard>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<NPagination
|
||||
v-model:page="pagination.page" v-model:page-size="pagination.pageSize"
|
||||
:item-count="pagination.itemCount" :page-sizes="pagination.pageSizes" show-size-picker
|
||||
@update:page="pagination.onChange" @update:page-size="pagination.onUpdatePageSize"
|
||||
>
|
||||
<template #prefix="{ itemCount }">
|
||||
共 {{ itemCount }} 项
|
||||
</template>
|
||||
</NPagination>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Add Question Drawer -->
|
||||
<NDrawer v-model:show="showQuestionModal" :width="800">
|
||||
<NDrawerContent :title="questionOperation === 'edit' ? '编辑题目详情' : '新增题目详情'">
|
||||
<NForm label-placement="top">
|
||||
<NFormItem label="题目正文内容">
|
||||
<NForm ref="questionFormRef" label-placement="top" :rules="rules" :model="questionForm" size="small">
|
||||
<NFormItem label="题目正文内容" path="name">
|
||||
<div class="w-full overflow-hidden border border-gray-200 rounded-lg">
|
||||
<!-- <RestBasicEditor v-model="questionForm.content" /> -->
|
||||
<NInput v-model:value="questionForm.content" type="textarea" class="h-64 w-full" />
|
||||
<!-- <RestBasicEditor v-model="questionForm.name" /> -->
|
||||
<NInput v-model:value="questionForm.name" type="textarea" class="h-64 w-full" />
|
||||
</div>
|
||||
</NFormItem>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- <div class="grid grid-cols-2 gap-4">
|
||||
<NFormItem label="参考分值 (Pts)">
|
||||
<NInputNumber v-model:value="questionForm.score" class="w-full" :min="1" />
|
||||
</NFormItem>
|
||||
<NFormItem label="限时 (S)">
|
||||
<NInputNumber v-model:value="questionForm.time" class="w-full" :min="1" />
|
||||
</NFormItem>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 上传图片 -->
|
||||
<NFormItem label="上传图片">
|
||||
<RestUpload v-model="questionForm.imageUrl" width="100px" height="100px" hint="请上传题目图片" />
|
||||
<!-- 类型 -->
|
||||
<NFormItem label="类型" path="type">
|
||||
<NSelect v-model:value="questionForm.type" class="w-full" :options="questionTypeOptions" />
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="参考标准答案">
|
||||
<!-- 是否优先使用 -->
|
||||
<NFormItem label="是否优先使用" path="IsGood">
|
||||
<NRadioGroup v-model:value="questionForm.IsGood">
|
||||
<NRadio :value="0">
|
||||
否
|
||||
</NRadio>
|
||||
<NRadio :value="1">
|
||||
是
|
||||
</NRadio>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
|
||||
<!-- 上传图片 -->
|
||||
<NFormItem v-if="questionForm.type === 1" label="上传图片">
|
||||
<NUpload
|
||||
v-model:file-list="fileList" accept="image/*" :max="1" list-type="image-card"
|
||||
:custom-request="customRequest" @change="handleUploadChange"
|
||||
>
|
||||
点击上传
|
||||
</NUpload>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem label="参考标准答案" path="answer">
|
||||
<NInput v-model:value="questionForm.answer" placeholder="正确答案" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { jsonClone } from '@sa/utils'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { enableStatusOptions } from '@/constants/business'
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'TemplateOperateDrawer',
|
||||
})
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
interface Props {
|
||||
operateType: NaiveUI.TableOperateType
|
||||
rowData?: Api.SystemManage.User | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void
|
||||
}
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false,
|
||||
})
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm()
|
||||
const { defaultRequiredRule } = useFormRules()
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<NaiveUI.TableOperateType, string> = {
|
||||
add: '新增模板',
|
||||
edit: '编辑模板',
|
||||
}
|
||||
return titles[props.operateType]
|
||||
})
|
||||
|
||||
type Model = Pick<
|
||||
Api.SystemManage.User,
|
||||
'userName' | 'userGender' | 'nickName' | 'userPhone' | 'userEmail' | 'userRoles' | 'status'
|
||||
>
|
||||
|
||||
const model = ref(createDefaultModel())
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
userName: '',
|
||||
userGender: null,
|
||||
nickName: '',
|
||||
userPhone: '',
|
||||
userEmail: '',
|
||||
userRoles: [],
|
||||
status: null,
|
||||
}
|
||||
}
|
||||
|
||||
type RuleKey = Extract<keyof Model, 'userName' | 'status'>
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
userName: defaultRequiredRule,
|
||||
status: defaultRequiredRule,
|
||||
}
|
||||
|
||||
function handleInitModel() {
|
||||
model.value = createDefaultModel()
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
Object.assign(model.value, jsonClone(props.rowData))
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate()
|
||||
// request
|
||||
window.$message?.success('更新成功')
|
||||
closeDrawer()
|
||||
emit('submitted')
|
||||
}
|
||||
|
||||
watch(visible, () => {
|
||||
if (visible.value) {
|
||||
handleInitModel()
|
||||
restoreValidation()
|
||||
}
|
||||
})
|
||||
|
||||
const competitionOptions = [
|
||||
{ label: '第九届阅读之星大赛', value: '1' },
|
||||
{ label: '科普阅读大赛', value: '2' },
|
||||
]
|
||||
|
||||
const sizeOptions = [
|
||||
{ label: '210mmx297mm', value: '1' },
|
||||
{ label: 'A3', value: '2' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem label="名称" path="userName">
|
||||
<NInput v-model:value="model.userName" placeholder="请输入名称" />
|
||||
</NFormItem>
|
||||
<NFormItem label="模板ID" path="nickName">
|
||||
<NInput v-model:value="model.nickName" placeholder="请输入模板ID" />
|
||||
</NFormItem>
|
||||
<NFormItem label="关联比赛" path="userGender">
|
||||
<NSelect
|
||||
v-model:value="model.userGender"
|
||||
:options="competitionOptions"
|
||||
placeholder="请选择关联比赛"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="设计尺寸" path="userPhone">
|
||||
<NSelect
|
||||
v-model:value="model.userPhone"
|
||||
:options="sizeOptions"
|
||||
placeholder="请选择设计尺寸"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem label="状态" path="status">
|
||||
<NRadioGroup v-model:value="model.status">
|
||||
<NRadio v-for="item in enableStatusOptions" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<NSpace :size="16">
|
||||
<NButton @click="closeDrawer">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">
|
||||
确认
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@ -1,92 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { jsonClone } from '@sa/utils'
|
||||
import { toRaw } from 'vue'
|
||||
import { enableStatusOptions } from '@/constants/business'
|
||||
import { useNaiveForm } from '@/hooks/common/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'TemplateSearch',
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
interface Emits {
|
||||
(e: 'search'): void
|
||||
}
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm()
|
||||
|
||||
const model = defineModel<Api.Template.TemplateSearchParams>('model', { required: true })
|
||||
|
||||
const defaultModel = jsonClone(toRaw(model.value))
|
||||
|
||||
function resetModel() {
|
||||
Object.assign(model.value, defaultModel)
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
await restoreValidation()
|
||||
resetModel()
|
||||
}
|
||||
|
||||
async function search() {
|
||||
await validate()
|
||||
emit('search')
|
||||
}
|
||||
|
||||
const competitionOptions = [
|
||||
{ label: '第九届阅读之星大赛', value: '1' },
|
||||
{ label: '科普阅读大赛', value: '2' },
|
||||
]
|
||||
|
||||
const sizeOptions = [
|
||||
{ label: '210mmx297mm', value: '1' },
|
||||
{ label: 'A3', value: '2' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard :bordered="false" size="small" class="card-wrapper">
|
||||
<NCollapse>
|
||||
<NCollapseItem title="基础信息" name="template-search">
|
||||
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
|
||||
<NGrid responsive="screen" item-responsive>
|
||||
<NFormItemGi span="24 s:12 m:6" label="模板名称" path="templateName" class="pr-24px">
|
||||
<NInput v-model:value="model.templateName" placeholder="请输入名称" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" label="关联比赛" path="competitionId" class="pr-24px">
|
||||
<NSelect
|
||||
v-model:value="model.competitionId" placeholder="请选择" :options="competitionOptions as any"
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" label="状态" path="status" class="pr-24px">
|
||||
<NSelect v-model:value="model.status" placeholder="请选择" :options="enableStatusOptions as any" clearable />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" label="尺寸" path="size" class="pr-24px">
|
||||
<NSelect v-model:value="model.size" placeholder="请选择" :options="sizeOptions" clearable />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24" class="pr-24px">
|
||||
<NSpace class="w-full" justify="end">
|
||||
<NButton @click="reset">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
重置
|
||||
</NButton>
|
||||
<NButton type="primary" ghost @click="search">
|
||||
<template #icon>
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
</template>
|
||||
搜索
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NFormItemGi>
|
||||
</NGrid>
|
||||
</NForm>
|
||||
</NCollapseItem>
|
||||
</NCollapse>
|
||||
</NCard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user