feat(admin): 更新题库管理和环境配置
- 禁用强制登录以便本地开发 - 更新开发环境API基础URL为本地服务 - 在代理配置中添加secure: false以支持HTTPS接口 - 修复请求响应中字符串类型数据的JSON解析问题 - 新增题目类型相关常量定义和类型声明 - 扩展transformRecordToOption函数支持数字键值 - 更新题库相关API接口和类型定义 - 在竞赛配置中添加题目分数字段 - 重构题库管理界面,实现分类和题目的完整CRUD操作 - 删除未使用的模板管理组件
This commit is contained in:
@ -25,7 +25,7 @@ VITE_ICON_LOCAL_PREFIX=icon-local
|
|||||||
VITE_AUTH_ROUTE_MODE=static
|
VITE_AUTH_ROUTE_MODE=static
|
||||||
|
|
||||||
# 是否强制登录: Y (开启) | N (关闭)
|
# 是否强制登录: Y (开启) | N (关闭)
|
||||||
VITE_AUTH_ROUTE_FORCE_LOGIN=Y
|
VITE_AUTH_ROUTE_FORCE_LOGIN=N
|
||||||
|
|
||||||
# 静态认证路由的主页
|
# 静态认证路由的主页
|
||||||
VITE_ROUTE_HOME=home
|
VITE_ROUTE_HOME=home
|
||||||
|
|||||||
@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
# VITE_SERVICE_BASE_URL=http://192.168.5.140:9999
|
# VITE_SERVICE_BASE_URL=http://192.168.5.140:9999
|
||||||
|
|
||||||
VITE_SERVICE_BASE_URL=https://api.qyzhjy.com
|
# VITE_SERVICE_BASE_URL=https://api.qyzhjy.com
|
||||||
|
|
||||||
# VITE_SERVICE_BASE_URL=http://172.16.10.130:5000
|
VITE_SERVICE_BASE_URL=https://172.16.10.130:5000
|
||||||
|
|
||||||
VITE_OTHER_SERVICE_BASE_URL= `{
|
VITE_OTHER_SERVICE_BASE_URL= `{
|
||||||
"demo": "http://localhost:9528"
|
"demo": "http://localhost:9528"
|
||||||
|
|||||||
@ -40,6 +40,7 @@ function createProxyItem(item: App.Service.ServiceConfigItem, enableLog: boolean
|
|||||||
proxy[item.proxyPattern] = {
|
proxy[item.proxyPattern] = {
|
||||||
target: item.baseURL,
|
target: item.baseURL,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
secure: false, // 如果是https接口,需要配置这个参数
|
||||||
configure: (_proxy, options) => {
|
configure: (_proxy, options) => {
|
||||||
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
_proxy.on('proxyReq', (_proxyReq, req, _res) => {
|
||||||
if (!enableLog)
|
if (!enableLog)
|
||||||
|
|||||||
@ -63,6 +63,13 @@ export const timeRecord: Record<Api.Competition.QuestionTimeType, string> = {
|
|||||||
90: '90s',
|
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 timeOptions: SelectOption[] = transformRecordToOption(timeRecord) as unknown as SelectOption[]
|
||||||
|
|
||||||
export const uiTypeOptions: SelectOption[] = transformRecordToOption(uiTypeRecord) as unknown as SelectOption[]
|
export const uiTypeOptions: SelectOption[] = transformRecordToOption(uiTypeRecord) as unknown as SelectOption[]
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
import { request } from '../request'
|
import { request } from '../request'
|
||||||
|
|
||||||
/** 获取所有题目列表 */
|
/** 获取所有题目列表 */
|
||||||
export function fetchGetQuestionListAll(params?: any) {
|
export function fetchGetQuestionListAll() {
|
||||||
return request<Api.Question.CommonRecord[]>({
|
return request<Api.Question.CommonRecord>({
|
||||||
url: '/Base/ActivityMain/GetQuestionListAll',
|
url: '/Base/ActivityMain/GetQuestionListAll',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: params || {},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** 新增题目 */
|
/** 新增题目 */
|
||||||
export function fetchAddQuestion(data?: Api.Question.AddParams) {
|
export function fetchAddQuestion(data?: Api.Question.AddParams) {
|
||||||
return request<Api.Question.CommonRecord>({
|
return request<Api.Question.CommonRecord>({
|
||||||
@ -19,3 +16,86 @@ export function fetchAddQuestion(data?: Api.Question.AddParams) {
|
|||||||
data: data || {},
|
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,
|
} as RequestInstanceState,
|
||||||
// 响应数据转换
|
// 响应数据转换
|
||||||
transform(response: AxiosResponse<App.Service.Response<any>>) {
|
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) {
|
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 {
|
namespace Question {
|
||||||
/** common params of paginating */
|
/** common params of paginating */
|
||||||
interface PaginatingCommonParams {
|
interface PaginatingCommonParams {
|
||||||
/** current page number */
|
/** pageIndex page number */
|
||||||
current: number
|
pageIndex: number
|
||||||
/** page size */
|
/** page size */
|
||||||
size: number
|
pageSizes: number
|
||||||
/** total count */
|
/** total count */
|
||||||
total: number
|
total: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** common params of paginating query list data */
|
/** common params of paginating query list data */
|
||||||
interface PaginatingQueryRecord<T = any> extends PaginatingCommonParams {
|
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 */
|
/** add params */
|
||||||
interface AddParams {
|
interface AddParams {
|
||||||
/** question id */
|
/** question id */
|
||||||
id: string
|
id: number
|
||||||
/** question name */
|
/** question name */
|
||||||
name: string
|
name: string
|
||||||
/** question content */
|
/** question content */
|
||||||
questionContent: string
|
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 */
|
/** common search params of table */
|
||||||
type CommonSearchParams = Pick<Common.PaginatingCommonParams, 'current' | 'size'>
|
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']
|
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSelect: typeof import('naive-ui')['NSelect']
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
|
NSelectOption: typeof import('naive-ui')['NSelectOption']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
NSpin: typeof import('naive-ui')['NSpin']
|
NSpin: typeof import('naive-ui')['NSpin']
|
||||||
NStatistic: typeof import('naive-ui')['NStatistic']
|
NStatistic: typeof import('naive-ui')['NStatistic']
|
||||||
@ -290,6 +291,7 @@ declare global {
|
|||||||
const NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
const NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||||
const NScrollbar: typeof import('naive-ui')['NScrollbar']
|
const NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
const NSelect: typeof import('naive-ui')['NSelect']
|
const NSelect: typeof import('naive-ui')['NSelect']
|
||||||
|
const NSelectOption: typeof import('naive-ui')['NSelectOption']
|
||||||
const NSpace: typeof import('naive-ui')['NSpace']
|
const NSpace: typeof import('naive-ui')['NSpace']
|
||||||
const NSpin: typeof import('naive-ui')['NSpin']
|
const NSpin: typeof import('naive-ui')['NSpin']
|
||||||
const NStatistic: typeof import('naive-ui')['NStatistic']
|
const NStatistic: typeof import('naive-ui')['NStatistic']
|
||||||
|
|||||||
@ -17,10 +17,11 @@ import { $t } from '@/locales'
|
|||||||
* ```;
|
* ```;
|
||||||
*
|
*
|
||||||
* @param record
|
* @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]) => ({
|
return Object.entries(record).map(([value, label]) => ({
|
||||||
value,
|
value: isNumberKey ? Number(value) : value,
|
||||||
label,
|
label,
|
||||||
})) as CommonType.Option<keyof T, T[keyof T]>[]
|
})) as CommonType.Option<keyof T, T[keyof T]>[]
|
||||||
}
|
}
|
||||||
@ -67,8 +68,8 @@ export function browserPathJoin(...paths: string[]) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 版本比较
|
* 版本比较
|
||||||
* @param v1
|
* @param v1
|
||||||
* @param v2
|
* @param v2
|
||||||
* @param separator 分隔符 默认.
|
* @param separator 分隔符 默认.
|
||||||
* @returns 1 v1>v2 | -1 v1<v2 | 0 v1==v2
|
* @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 s1 = v1.split(separator)
|
||||||
const s2 = v2.split(separator)
|
const s2 = v2.split(separator)
|
||||||
const len = Math.max(s1.length, s2.length)
|
const len = Math.max(s1.length, s2.length)
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
for (let i = 0; i < len; i++) {
|
||||||
const num1 = parseInt(s1[i] || '0')
|
const num1 = Number.parseInt(s1[i] || '0')
|
||||||
const num2 = parseInt(s2[i] || '0')
|
const num2 = Number.parseInt(s2[i] || '0')
|
||||||
if (num1 > num2) return 1
|
if (num1 > num2)
|
||||||
if (num1 < num2) return -1
|
return 1
|
||||||
|
if (num1 < num2)
|
||||||
|
return -1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ const props = defineProps<{
|
|||||||
time: number
|
time: number
|
||||||
title: string
|
title: string
|
||||||
scoreType: Api.Competition.QuestionScoreType
|
scoreType: Api.Competition.QuestionScoreType
|
||||||
|
score: number
|
||||||
uiType?: string
|
uiType?: string
|
||||||
templateId?: string
|
templateId?: string
|
||||||
}>
|
}>
|
||||||
@ -292,7 +293,7 @@ defineExpose({ validate, reset })
|
|||||||
</div>
|
</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" />
|
<icon-ic-outline-timer class="text-gray-400" />
|
||||||
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">答题时间:</span>
|
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">答题时间:</span>
|
||||||
<NSelect
|
<NSelect
|
||||||
@ -303,13 +304,23 @@ defineExpose({ validate, reset })
|
|||||||
</div>
|
</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" />
|
<icon-streamline-sharp:type-area-remix class="text-gray-400" />
|
||||||
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">分数规则:</span>
|
<span class="whitespace-nowrap text-sm text-gray-400 font-bold">分数规则:</span>
|
||||||
<NSelect
|
<NSelect
|
||||||
v-model:value="item.scoreType" :options="scoreTypeOptions as unknown as SelectOption[]"
|
v-model:value="item.scoreType" :options="scoreTypeOptions as unknown as SelectOption[]"
|
||||||
class="flex-1 !bg-transparent" size="small" :bordered="false"
|
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>
|
<span class="text-xs text-gray-400 font-bold">分</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,8 +8,9 @@ import {
|
|||||||
useDialog,
|
useDialog,
|
||||||
useMessage,
|
useMessage,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { ref, watch } from 'vue'
|
import { onMounted, ref, watch } from 'vue'
|
||||||
import SvgIcon from '@/components/custom/svg-icon.vue'
|
import SvgIcon from '@/components/custom/svg-icon.vue'
|
||||||
|
import { fetchAddQuestion, fetchDeleteQuestion, fetchGetQuestionListAll, fetchUpdateQuestion } from '@/service/api/question'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:category', node: any): void
|
(e: 'update:category', node: any): void
|
||||||
@ -19,39 +20,19 @@ const message = useMessage()
|
|||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
|
||||||
interface CategoryItem {
|
interface CategoryItem {
|
||||||
key: string
|
Id: number
|
||||||
label: string
|
Name: string
|
||||||
|
QuestionContent: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock Data - 模拟分类列表数据
|
// Mock Data - 模拟分类列表数据
|
||||||
const categoryList = ref<CategoryItem[]>([
|
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 selectedKey = ref<string | null>(null)
|
const selectedKey = ref<number | null>(null)
|
||||||
|
|
||||||
// Modal State
|
// Modal State
|
||||||
const showCategoryModal = ref(false)
|
const showCategoryModal = ref(false)
|
||||||
const categoryForm = ref({ name: '' })
|
const categoryForm = ref({ questionContent: '', name: '', Id: 0 })
|
||||||
const categoryOperation = ref<'add' | 'edit'>('add')
|
const categoryOperation = ref<'add' | 'edit'>('add')
|
||||||
const currentOperationNode = ref<CategoryItem | null>(null)
|
const currentOperationNode = ref<CategoryItem | null>(null)
|
||||||
|
|
||||||
@ -60,12 +41,10 @@ watch(selectedKey, (newKey) => {
|
|||||||
emit('update:category', null)
|
emit('update:category', null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const node = categoryList.value.find(item => item.key === newKey)
|
const node = categoryList.value.find(item => item.Id === newKey)
|
||||||
if (node) {
|
if (node) {
|
||||||
emit('update:category', {
|
emit('update:category', {
|
||||||
...node,
|
...node,
|
||||||
level: 1, // Treat all as level 1
|
|
||||||
isLeaf: true, // Treat all as leaf (container for questions)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -73,12 +52,13 @@ watch(selectedKey, (newKey) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleSelect(key: string) {
|
function handleSelect(key: number) {
|
||||||
selectedKey.value = key
|
selectedKey.value = key
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAddCategory() {
|
function handleAddCategory() {
|
||||||
categoryOperation.value = 'add'
|
categoryOperation.value = 'add'
|
||||||
|
categoryForm.value.questionContent = ''
|
||||||
categoryForm.value.name = ''
|
categoryForm.value.name = ''
|
||||||
currentOperationNode.value = null
|
currentOperationNode.value = null
|
||||||
showCategoryModal.value = true
|
showCategoryModal.value = true
|
||||||
@ -86,7 +66,8 @@ function handleAddCategory() {
|
|||||||
|
|
||||||
function handleEditCategory(item: CategoryItem) {
|
function handleEditCategory(item: CategoryItem) {
|
||||||
categoryOperation.value = 'edit'
|
categoryOperation.value = 'edit'
|
||||||
categoryForm.value.name = item.label
|
categoryForm.value.name = item.Name
|
||||||
|
categoryForm.value.questionContent = item.QuestionContent
|
||||||
currentOperationNode.value = item
|
currentOperationNode.value = item
|
||||||
showCategoryModal.value = true
|
showCategoryModal.value = true
|
||||||
}
|
}
|
||||||
@ -94,54 +75,98 @@ function handleEditCategory(item: CategoryItem) {
|
|||||||
function handleDeleteCategory(item: CategoryItem) {
|
function handleDeleteCategory(item: CategoryItem) {
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
title: '警告',
|
title: '警告',
|
||||||
content: `确定要删除分类 "${item.label}" 吗?此操作无法撤销。`,
|
content: `确定要删除分类 "${item.Name}" 吗?此操作无法撤销。`,
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: () => {
|
onPositiveClick: async () => {
|
||||||
const index = categoryList.value.findIndex(n => n.key === item.key)
|
const { error } = await fetchDeleteQuestion(item.Id)
|
||||||
|
if (error) {
|
||||||
|
message.error('删除分类失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const index = categoryList.value.findIndex(n => n.Id === item.Id)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
categoryList.value.splice(index, 1)
|
categoryList.value.splice(index, 1)
|
||||||
if (selectedKey.value === item.key) {
|
if (selectedKey.value === item.Id) {
|
||||||
selectedKey.value = null
|
selectedKey.value = null
|
||||||
}
|
}
|
||||||
|
// 获取最新分类列表
|
||||||
|
await fetchCategoryList()
|
||||||
message.success('删除成功')
|
message.success('删除成功')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitCategory() {
|
/**
|
||||||
|
* 新增分类|更新分类
|
||||||
|
* @param data 新增分类参数|更新分类参数
|
||||||
|
*/
|
||||||
|
async function submitCategory() {
|
||||||
if (!categoryForm.value.name) {
|
if (!categoryForm.value.name) {
|
||||||
message.error('请输入分类名称')
|
message.error('请输入分类名称')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryOperation.value === 'add') {
|
if (categoryOperation.value === 'add') {
|
||||||
const newKey = `cate-${Date.now()}`
|
const { error } = await fetchAddQuestion({
|
||||||
categoryList.value.push({
|
questionContent: categoryForm.value.questionContent,
|
||||||
key: newKey,
|
name: categoryForm.value.name,
|
||||||
label: categoryForm.value.name,
|
id: 0,
|
||||||
})
|
})
|
||||||
|
if (error) {
|
||||||
|
message.error('添加分类失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await fetchCategoryList()
|
||||||
message.success('分类添加成功')
|
message.success('分类添加成功')
|
||||||
// Automatically select the new category
|
|
||||||
selectedKey.value = newKey
|
|
||||||
}
|
}
|
||||||
else if (categoryOperation.value === 'edit' && currentOperationNode.value) {
|
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('分类修改成功')
|
message.success('分类修改成功')
|
||||||
// Trigger update if currently selected
|
await fetchCategoryList()
|
||||||
if (selectedKey.value === currentOperationNode.value.key) {
|
// 更新选中分类的信息
|
||||||
|
if (selectedKey.value === currentOperationNode.value.Id) {
|
||||||
const node = currentOperationNode.value
|
const node = currentOperationNode.value
|
||||||
emit('update:category', {
|
emit('update:category', {
|
||||||
...node,
|
...node,
|
||||||
level: 1,
|
Name: categoryForm.value.name,
|
||||||
isLeaf: true,
|
QuestionContent: categoryForm.value.questionContent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showCategoryModal.value = false
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -161,17 +186,17 @@ function submitCategory() {
|
|||||||
暂无分类,请点击上方添加
|
暂无分类,请点击上方添加
|
||||||
</div>
|
</div>
|
||||||
<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="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'"
|
:class="selectedKey === item.Id ? 'bg-white shadow-sm text-primary' : 'text-gray-600'"
|
||||||
@click="handleSelect(item.key)"
|
@click="handleSelect(item.Id)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2 overflow-hidden">
|
<div class="flex items-center gap-2 overflow-hidden">
|
||||||
<SvgIcon
|
<SvgIcon
|
||||||
icon="carbon:folder" class="flex-shrink-0 text-lg"
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
@ -205,6 +230,9 @@ function submitCategory() {
|
|||||||
<NFormItem label="分类名称">
|
<NFormItem label="分类名称">
|
||||||
<NInput v-model:value="categoryForm.name" placeholder="请输入分类名称" @keyup.enter="submitCategory" />
|
<NInput v-model:value="categoryForm.name" placeholder="请输入分类名称" @keyup.enter="submitCategory" />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
<NFormItem label="分类描述">
|
||||||
|
<NInput v-model:value="categoryForm.questionContent" placeholder="请输入分类描述" @keyup.enter="submitCategory" />
|
||||||
|
</NFormItem>
|
||||||
</NForm>
|
</NForm>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { FormInst, UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui'
|
||||||
import {
|
import {
|
||||||
NBreadcrumb,
|
NBreadcrumb,
|
||||||
NBreadcrumbItem,
|
NBreadcrumbItem,
|
||||||
@ -9,181 +10,241 @@ import {
|
|||||||
NEmpty,
|
NEmpty,
|
||||||
NForm,
|
NForm,
|
||||||
NFormItem,
|
NFormItem,
|
||||||
|
NImage,
|
||||||
NInput,
|
NInput,
|
||||||
NInputNumber,
|
NPagination,
|
||||||
|
NRadio,
|
||||||
|
NRadioGroup,
|
||||||
|
NSelect,
|
||||||
NTag,
|
NTag,
|
||||||
|
NUpload,
|
||||||
|
useDialog,
|
||||||
|
useMessage,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import SvgIcon from '@/components/custom/svg-icon.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<{
|
const props = defineProps<{
|
||||||
currentCategory: any
|
currentCategory: any
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Mock Data - 模拟题目列表数据
|
const message = useMessage()
|
||||||
const questionList = ref([
|
const dialog = useDialog()
|
||||||
{
|
|
||||||
id: 'Q-0325',
|
// 题目列表数据
|
||||||
categoryKey: 'stage-1',
|
const questionList = ref<any[]>([])
|
||||||
content: 'jiū 表示小鸟的叫声',
|
const loading = ref(false)
|
||||||
answer: '碧',
|
|
||||||
score: 10,
|
const pagination = ref({
|
||||||
time: 30,
|
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)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
onUpdatePageSize: (pageSize: number) => {
|
||||||
id: 'Q-0326',
|
pagination.value.pageSize = pageSize
|
||||||
categoryKey: 'stage-1',
|
pagination.value.page = 1
|
||||||
content: '“春色满园关不住,一枝红杏出墙来”。请书写“杏”字。',
|
if (props.currentCategory?.Id) {
|
||||||
answer: '杏',
|
fetchData(props.currentCategory.Id)
|
||||||
score: 10,
|
}
|
||||||
time: 30,
|
|
||||||
},
|
},
|
||||||
{
|
})
|
||||||
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 searchText = ref('')
|
||||||
const showQuestionModal = ref(false)
|
const showQuestionModal = ref(false)
|
||||||
const questionOperation = ref<'add' | 'edit'>('add')
|
const questionOperation = ref<'add' | 'edit'>('add')
|
||||||
const currentQuestionId = ref<string | null>(null)
|
const currentQuestionId = ref<number | null>(null)
|
||||||
const questionForm = ref({
|
const questionForm = ref({
|
||||||
content: '',
|
name: '',
|
||||||
answer: '',
|
answer: '',
|
||||||
score: 10,
|
type: 0,
|
||||||
time: 30,
|
|
||||||
imageUrl: '',
|
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
|
return !!props.currentCategory
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredQuestions = computed(() => {
|
watch(() => props.currentCategory, (newVal) => {
|
||||||
let list = questionList.value
|
if (newVal && newVal.Id) {
|
||||||
|
pagination.value.page = 1
|
||||||
// 1. Filter by Category Key
|
searchText.value = ''
|
||||||
if (props.currentCategory && props.currentCategory.key) {
|
fetchData(newVal.Id)
|
||||||
list = list.filter(q => q.categoryKey === props.currentCategory.key)
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
// 2. Filter by Search Text
|
questionList.value = []
|
||||||
if (searchText.value) {
|
|
||||||
list = list.filter(q => q.content.includes(searchText.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() {
|
function handleAddQuestion() {
|
||||||
if (!props.currentCategory) {
|
if (!props.currentCategory) {
|
||||||
window.$message?.warning('请先选择一个分类')
|
message.warning('请先选择一个分类')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
questionOperation.value = 'add'
|
questionOperation.value = 'add'
|
||||||
currentQuestionId.value = null
|
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
|
showQuestionModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditQuestion(id: string) {
|
function handleEditQuestion(id: number) {
|
||||||
const question = questionList.value.find(q => q.id === id)
|
const question = questionList.value.find(q => q.Id === id)
|
||||||
if (question) {
|
if (question) {
|
||||||
questionOperation.value = 'edit'
|
questionOperation.value = 'edit'
|
||||||
currentQuestionId.value = id
|
currentQuestionId.value = id
|
||||||
questionForm.value = {
|
questionForm.value = {
|
||||||
content: question.content,
|
name: question.Name,
|
||||||
answer: question.answer,
|
answer: question.Answer,
|
||||||
score: question.score,
|
type: question.Type,
|
||||||
time: question.time,
|
imageUrl: question.ImageUrl || '',
|
||||||
imageUrl: (question as any).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
|
showQuestionModal.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeleteQuestion(id: string) {
|
function handleDeleteQuestion(id: number) {
|
||||||
window.$dialog?.warning({
|
dialog.warning({
|
||||||
title: '警告',
|
title: '警告',
|
||||||
content: '确定要删除这道题目吗?此操作无法撤销。',
|
content: '确定要删除这道题目吗?此操作无法撤销。',
|
||||||
positiveText: '确定',
|
positiveText: '确定',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: () => {
|
onPositiveClick: async () => {
|
||||||
const index = questionList.value.findIndex(q => q.id === id)
|
const { error } = await fetchDeleteQuestionLibrary([id])
|
||||||
if (index !== -1) {
|
if (!error) {
|
||||||
questionList.value.splice(index, 1)
|
message.success('删除成功')
|
||||||
window.$message?.success('删除成功')
|
if (props.currentCategory?.Id) {
|
||||||
|
fetchData(props.currentCategory.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message.error('删除失败')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitQuestion() {
|
async function submitQuestion() {
|
||||||
if (!questionForm.value.content || !questionForm.value.answer) {
|
const valid = await questionFormRef.value?.validate()
|
||||||
window.$message?.error('请填写完整信息')
|
|
||||||
|
if (!valid) {
|
||||||
|
message.error('请填写完整信息')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('questionForm.value', questionForm.value)
|
|
||||||
|
|
||||||
if (questionOperation.value === 'add') {
|
const commonParams = {
|
||||||
// Mock add
|
name: questionForm.value.name,
|
||||||
questionList.value.push({
|
answer: questionForm.value.answer,
|
||||||
id: `Q-${Math.floor(Math.random() * 10000)}`,
|
imageUrl: '',
|
||||||
...questionForm.value,
|
type: questionForm.value.type as any,
|
||||||
categoryKey: props.currentCategory?.key, // Add categoryKey
|
questionId: props.currentCategory.Id,
|
||||||
})
|
IsGood: questionForm.value.IsGood,
|
||||||
window.$message?.success('题目添加成功')
|
|
||||||
}
|
}
|
||||||
else if (questionOperation.value === 'edit' && currentQuestionId.value) {
|
|
||||||
const index = questionList.value.findIndex(q => q.id === currentQuestionId.value)
|
let fileToUpload: File | null = null
|
||||||
if (index !== -1) {
|
if (questionForm.value.type === 1) { // 图片类型 (1: 图片, 0: 文字 - based on previous fixes)
|
||||||
questionList.value[index] = {
|
if (fileList.value.length > 0 && fileList.value[0].file) {
|
||||||
...questionList.value[index],
|
fileToUpload = fileList.value[0].file
|
||||||
...questionForm.value,
|
}
|
||||||
}
|
else if (questionOperation.value === 'add' && fileList.value.length === 0) {
|
||||||
window.$message?.success('题目修改成功')
|
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>
|
</script>
|
||||||
|
|
||||||
@ -194,20 +255,20 @@ function submitQuestion() {
|
|||||||
<NBreadcrumb>
|
<NBreadcrumb>
|
||||||
<NBreadcrumbItem>题库全集</NBreadcrumbItem>
|
<NBreadcrumbItem>题库全集</NBreadcrumbItem>
|
||||||
<NBreadcrumbItem v-if="currentCategory && currentCategory.level === 1">
|
<NBreadcrumbItem v-if="currentCategory && currentCategory.level === 1">
|
||||||
{{ currentCategory.label }}
|
{{ currentCategory.name }}
|
||||||
</NBreadcrumbItem>
|
</NBreadcrumbItem>
|
||||||
</NBreadcrumb>
|
</NBreadcrumb>
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h1 class="m-0 text-xl text-gray-800 font-bold">
|
<h1 class="m-0 text-xl text-gray-800 font-bold">
|
||||||
{{ currentCategory ? currentCategory.label : '' }}
|
{{ currentCategory ? currentCategory.name : '' }}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索框 -->
|
<!-- 搜索框 -->
|
||||||
<div v-if="currentCategory" class="mt-2 flex items-center justify-between">
|
<div v-if="currentCategory" class="mt-2 flex items-center justify-between">
|
||||||
<div class="flex items-center gap-3">
|
<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>
|
<template #prefix>
|
||||||
<SvgIcon icon="carbon:search" class="text-gray-400" />
|
<SvgIcon icon="carbon:search" class="text-gray-400" />
|
||||||
</template>
|
</template>
|
||||||
@ -225,7 +286,7 @@ function submitQuestion() {
|
|||||||
|
|
||||||
<!-- 题目列表区域 -->
|
<!-- 题目列表区域 -->
|
||||||
<div class="flex-1 overflow-y-auto bg-gray-50/50 p-6">
|
<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">
|
<div class="h-full flex flex-col items-center justify-center text-gray-400">
|
||||||
<NEmpty description="暂无数据">
|
<NEmpty description="暂无数据">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
@ -235,7 +296,7 @@ function submitQuestion() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="filteredQuestions.length === 0">
|
<template v-else-if="questionList.length === 0">
|
||||||
<div class="mt-20 flex justify-center">
|
<div class="mt-20 flex justify-center">
|
||||||
<NEmpty description="该分类下暂无题目" />
|
<NEmpty description="该分类下暂无题目" />
|
||||||
</div>
|
</div>
|
||||||
@ -244,20 +305,28 @@ function submitQuestion() {
|
|||||||
<!-- 题目列表 -->
|
<!-- 题目列表 -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<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>
|
<template #header>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<NTag size="small" type="primary" :bordered="false">
|
<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>
|
</NTag>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #header-extra>
|
<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>
|
</template>
|
||||||
|
|
||||||
<div class="py-2 text-base text-gray-700 font-medium">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- 标准答案 -->
|
<!-- 标准答案 -->
|
||||||
@ -266,19 +335,19 @@ function submitQuestion() {
|
|||||||
STANDARD ANSWER
|
STANDARD ANSWER
|
||||||
</div>
|
</div>
|
||||||
<div class="text-green-800 font-bold">
|
<div class="text-green-800 font-bold">
|
||||||
{{ q.answer }}
|
{{ q.Answer }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 操作按钮 -->
|
<!-- 操作按钮 -->
|
||||||
<template #action>
|
<template #action>
|
||||||
<div class="flex justify-end gap-2">
|
<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>
|
<template #icon>
|
||||||
<SvgIcon icon="carbon:edit" />
|
<SvgIcon icon="carbon:edit" />
|
||||||
</template>
|
</template>
|
||||||
</NButton>
|
</NButton>
|
||||||
<NButton size="tiny" quaternary type="error" @click="handleDeleteQuestion(q.id)">
|
<NButton size="tiny" quaternary type="error" @click="handleDeleteQuestion(q.Id)">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon icon="carbon:trash-can" />
|
<SvgIcon icon="carbon:trash-can" />
|
||||||
</template>
|
</template>
|
||||||
@ -287,35 +356,68 @@ function submitQuestion() {
|
|||||||
</template>
|
</template>
|
||||||
</NCard>
|
</NCard>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Question Drawer -->
|
<!-- Add Question Drawer -->
|
||||||
<NDrawer v-model:show="showQuestionModal" :width="800">
|
<NDrawer v-model:show="showQuestionModal" :width="800">
|
||||||
<NDrawerContent :title="questionOperation === 'edit' ? '编辑题目详情' : '新增题目详情'">
|
<NDrawerContent :title="questionOperation === 'edit' ? '编辑题目详情' : '新增题目详情'">
|
||||||
<NForm label-placement="top">
|
<NForm ref="questionFormRef" label-placement="top" :rules="rules" :model="questionForm" size="small">
|
||||||
<NFormItem label="题目正文内容">
|
<NFormItem label="题目正文内容" path="name">
|
||||||
<div class="w-full overflow-hidden border border-gray-200 rounded-lg">
|
<div class="w-full overflow-hidden border border-gray-200 rounded-lg">
|
||||||
<!-- <RestBasicEditor v-model="questionForm.content" /> -->
|
<!-- <RestBasicEditor v-model="questionForm.name" /> -->
|
||||||
<NInput v-model:value="questionForm.content" type="textarea" class="h-64 w-full" />
|
<NInput v-model:value="questionForm.name" type="textarea" class="h-64 w-full" />
|
||||||
</div>
|
</div>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<!-- <div class="grid grid-cols-2 gap-4">
|
||||||
<NFormItem label="参考分值 (Pts)">
|
<NFormItem label="参考分值 (Pts)">
|
||||||
<NInputNumber v-model:value="questionForm.score" class="w-full" :min="1" />
|
<NInputNumber v-model:value="questionForm.score" class="w-full" :min="1" />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
<NFormItem label="限时 (S)">
|
<NFormItem label="限时 (S)">
|
||||||
<NInputNumber v-model:value="questionForm.time" class="w-full" :min="1" />
|
<NInputNumber v-model:value="questionForm.time" class="w-full" :min="1" />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- 上传图片 -->
|
<!-- 类型 -->
|
||||||
<NFormItem label="上传图片">
|
<NFormItem label="类型" path="type">
|
||||||
<RestUpload v-model="questionForm.imageUrl" width="100px" height="100px" hint="请上传题目图片" />
|
<NSelect v-model:value="questionForm.type" class="w-full" :options="questionTypeOptions" />
|
||||||
</NFormItem>
|
</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="正确答案" />
|
<NInput v-model:value="questionForm.answer" placeholder="正确答案" />
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
</NForm>
|
</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