feat(admin): 新增题库、排行榜、实时结果和模板管理功能
- 添加题库管理页面及相关组件 - 实现排行榜管理功能,包括列表和详情页 - 新增实时结果展示页面 - 添加模板制作和管理功能 - 完善路由配置和国际化支持 - 新增阿里云OSS文件上传服务 - 添加多种工具函数和类型定义 - 优化表格和表单组件 - 调整布局和样式细节
This commit is contained in:
@ -27,7 +27,8 @@ function onMouseDown(e: MouseEvent) {
|
||||
}
|
||||
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
if (!isDragging.value)
|
||||
return
|
||||
const dx = e.clientX - startX.value
|
||||
const newWidth = startWidth.value + dx
|
||||
|
||||
@ -48,12 +49,13 @@ function onMouseUp() {
|
||||
<template>
|
||||
<div class="h-full flex overflow-hidden rounded-2xl bg-white shadow-sm">
|
||||
<!-- 分类树区域 -->
|
||||
<div class="h-full flex-shrink-0 relative" :style="{ width: `${siderWidth}px` }">
|
||||
<div class="relative h-full flex-shrink-0" :style="{ width: `${siderWidth}px` }">
|
||||
<CategoryTree @update:category="val => currentCategory = val" />
|
||||
<!-- 分类树宽度调整手柄 -->
|
||||
<div
|
||||
class="absolute top-0 right-0 h-full w-1 cursor-col-resize hover:bg-primary/20 active:bg-primary/40 transition-colors z-10"
|
||||
@mousedown="onMouseDown"></div>
|
||||
class="absolute right-0 top-0 z-10 h-full w-1 cursor-col-resize transition-colors active:bg-primary/40 hover:bg-primary/20"
|
||||
@mousedown="onMouseDown"
|
||||
/>
|
||||
</div>
|
||||
<!-- 题目列表区域 -->
|
||||
<div class="h-full flex-1 overflow-hidden">
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, h, watch } from 'vue'
|
||||
import {
|
||||
NButton,
|
||||
NTree,
|
||||
NModal,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
useMessage,
|
||||
NModal,
|
||||
NTree,
|
||||
type TreeOption,
|
||||
useDialog,
|
||||
type TreeOption
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { h, ref, watch } from 'vue'
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -135,9 +135,10 @@ watch(selectedKeys, (newKeys) => {
|
||||
level: node.isLeaf ? 3 : 1,
|
||||
key: node.key,
|
||||
isLeaf: node.isLeaf,
|
||||
children: node.children
|
||||
children: node.children,
|
||||
})
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
emit('update:category', null)
|
||||
}
|
||||
})
|
||||
@ -318,10 +319,12 @@ function renderSuffix({ option }: { option: TreeOption }) {
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto py-2">
|
||||
<NTree block-line :data="treeData" :selected-keys="selectedKeys" :expanded-keys="expandedKeys"
|
||||
<NTree
|
||||
block-line :data="treeData" :selected-keys="selectedKeys" :expanded-keys="expandedKeys"
|
||||
:render-prefix="renderPrefix" :render-suffix="renderSuffix" selectable expand-on-click class="px-2"
|
||||
@update:selected-keys="(keys) => (selectedKeys = keys)"
|
||||
@update:expanded-keys="(keys) => (expandedKeys = keys)" />
|
||||
@update:expanded-keys="(keys) => (expandedKeys = keys)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-100 bg-gray-50 p-3 text-xs text-gray-400">
|
||||
@ -329,8 +332,10 @@ function renderSuffix({ option }: { option: TreeOption }) {
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Category Modal -->
|
||||
<NModal v-model:show="showCategoryModal" preset="card"
|
||||
:title="categoryOperation === 'edit' ? '编辑分类' : `新增 ${categoryModalType} 级分类`" class="w-[500px]">
|
||||
<NModal
|
||||
v-model:show="showCategoryModal" preset="card"
|
||||
:title="categoryOperation === 'edit' ? '编辑分类' : `新增 ${categoryModalType} 级分类`" class="w-[500px]"
|
||||
>
|
||||
<NForm>
|
||||
<NFormItem label="分类显示名称">
|
||||
<NInput v-model:value="categoryForm.name" placeholder="请输入内容" />
|
||||
|
||||
@ -1,22 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
NBreadcrumb,
|
||||
NBreadcrumbItem,
|
||||
NButton,
|
||||
NCard,
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
NEmpty,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NInputNumber,
|
||||
NDrawer,
|
||||
NDrawerContent,
|
||||
NTag,
|
||||
|
||||
} from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import SvgIcon from '@/components/custom/svg-icon.vue'
|
||||
import RestBasicEditor from '@/components/common/rest-basic-editor/rest-basic-editor.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
currentCategory: any
|
||||
@ -168,7 +167,7 @@ function submitQuestion() {
|
||||
questionList.value.push({
|
||||
id: `Q-${Math.floor(Math.random() * 10000)}`,
|
||||
...questionForm.value,
|
||||
categoryKey: props.currentCategory?.key // Add categoryKey
|
||||
categoryKey: props.currentCategory?.key, // Add categoryKey
|
||||
})
|
||||
window.$message?.success('题目添加成功')
|
||||
}
|
||||
@ -294,9 +293,9 @@ function submitQuestion() {
|
||||
<NDrawerContent :title="questionOperation === 'edit' ? '编辑题目详情' : '新增题目详情'">
|
||||
<NForm label-placement="top">
|
||||
<NFormItem label="题目正文内容">
|
||||
<div class="w-full border border-gray-200 rounded-lg overflow-hidden">
|
||||
<div class="w-full overflow-hidden border border-gray-200 rounded-lg">
|
||||
<!-- <RestBasicEditor v-model="questionForm.content" /> -->
|
||||
<NInput type="textarea" v-model:value="questionForm.content" class="w-full h-64" />
|
||||
<NInput v-model:value="questionForm.content" type="textarea" class="h-64 w-full" />
|
||||
</div>
|
||||
</NFormItem>
|
||||
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<script setup lang="tsx">
|
||||
import { reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { NButton, NTag, NCard, NSpace } from 'naive-ui';
|
||||
import { fetchGetUserList } from '@/service/api';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
|
||||
import RankSearch from './modules/rank-search.vue';
|
||||
import { NButton, NCard, NTag } from 'naive-ui'
|
||||
import { reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table'
|
||||
import { fetchGetUserList } from '@/service/api'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import RankSearch from './modules/rank-search.vue'
|
||||
|
||||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
const searchParams: Api.SystemManage.UserSearchParams = reactive({
|
||||
current: 1,
|
||||
@ -18,67 +18,67 @@ const searchParams: Api.SystemManage.UserSearchParams = reactive({
|
||||
userGender: null,
|
||||
nickName: null,
|
||||
userPhone: null,
|
||||
userEmail: null
|
||||
});
|
||||
userEmail: null,
|
||||
})
|
||||
|
||||
const rankTitle = '排行榜管理列表';
|
||||
const rankTitle = '排行榜管理列表'
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
1: '已发布',
|
||||
2: '待审核'
|
||||
};
|
||||
2: '待审核',
|
||||
}
|
||||
|
||||
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination } = useNaivePaginatedTable({
|
||||
api: () => fetchGetUserList(searchParams),
|
||||
transform: response => {
|
||||
const transformed = defaultTransform(response);
|
||||
transform: (response) => {
|
||||
const transformed = defaultTransform(response)
|
||||
// Mocking data for Rank Management based on User API response
|
||||
const mockCompetitions = ['阅读之星-辞海遨游环节 (2026)', '古诗词大会年度精英巅峰赛', '趣味百科常识挑战周'];
|
||||
const mockDates = ['2026.01.12', '2026.01.15', '2026.01.18'];
|
||||
const mockScales = ['40 支队伍', '24 支队伍', '60 支队伍'];
|
||||
const mockScores = ['145', '180', '120'];
|
||||
const mockTimes = ['2026-01-22 16:30:12', '2026-01-22 12:00:00', '2026-01-21 18:22:45'];
|
||||
const mockCompetitions = ['阅读之星-辞海遨游环节 (2026)', '古诗词大会年度精英巅峰赛', '趣味百科常识挑战周']
|
||||
const mockDates = ['2026.01.12', '2026.01.15', '2026.01.18']
|
||||
const mockScales = ['40 支队伍', '24 支队伍', '60 支队伍']
|
||||
const mockScores = ['145', '180', '120']
|
||||
const mockTimes = ['2026-01-22 16:30:12', '2026-01-22 12:00:00', '2026-01-21 18:22:45']
|
||||
|
||||
transformed.data.forEach((item, index) => {
|
||||
const mockIndex = index % 3;
|
||||
item.userName = mockCompetitions[mockIndex]; // Competition Name
|
||||
item.userEmail = mockDates[mockIndex]; // Activity Date (using userEmail field)
|
||||
item.userPhone = mockScales[mockIndex]; // Team Scale (using userPhone field)
|
||||
item.nickName = mockScores[mockIndex]; // Max Score (using nickName field)
|
||||
const mockIndex = index % 3
|
||||
item.userName = mockCompetitions[mockIndex] // Competition Name
|
||||
item.userEmail = mockDates[mockIndex] // Activity Date (using userEmail field)
|
||||
item.userPhone = mockScales[mockIndex] // Team Scale (using userPhone field)
|
||||
item.nickName = mockScores[mockIndex] // Max Score (using nickName field)
|
||||
// item.createTime is typically available, we'll simulate update time
|
||||
item.createTime = mockTimes[mockIndex];
|
||||
item.createTime = mockTimes[mockIndex]
|
||||
// Status: 1 (Published), 2 (Pending)
|
||||
item.status = (index % 2 === 0) ? '2' : '1';
|
||||
});
|
||||
return transformed;
|
||||
item.status = (index % 2 === 0) ? '2' : '1'
|
||||
})
|
||||
return transformed
|
||||
},
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.current = params.page;
|
||||
searchParams.size = params.pageSize;
|
||||
onPaginationParamsChange: (params) => {
|
||||
searchParams.current = params.page
|
||||
searchParams.size = params.pageSize
|
||||
},
|
||||
columns: () => [
|
||||
{
|
||||
type: 'selection',
|
||||
align: 'center',
|
||||
width: 48
|
||||
width: 48,
|
||||
},
|
||||
{
|
||||
key: 'userName',
|
||||
title: '比赛活动名称',
|
||||
align: 'left',
|
||||
minWidth: 200
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
key: 'userEmail',
|
||||
title: '活动日期',
|
||||
align: 'center',
|
||||
minWidth: 120
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
key: 'userPhone',
|
||||
title: '队伍规模',
|
||||
align: 'center',
|
||||
minWidth: 100
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
key: 'nickName',
|
||||
@ -86,26 +86,31 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
|
||||
align: 'center',
|
||||
minWidth: 100,
|
||||
render: row => (
|
||||
<span class="text-primary font-bold">{row.nickName} Pts</span>
|
||||
)
|
||||
<span class="text-primary font-bold">
|
||||
{row.nickName}
|
||||
{' '}
|
||||
Pts
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
title: '最后更新时间',
|
||||
align: 'center',
|
||||
minWidth: 160
|
||||
minWidth: 160,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
title: '发布状态',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: row => {
|
||||
if (row.status === null) return null;
|
||||
const label = statusMap[row.status] || '未知';
|
||||
render: (row) => {
|
||||
if (row.status === null)
|
||||
return null
|
||||
const label = statusMap[row.status] || '未知'
|
||||
// 1: Published (Success/Green), 2: Pending (Warning/Orange)
|
||||
return <NTag type={row.status === '1' ? 'success' : 'warning'}>{label}</NTag>;
|
||||
}
|
||||
return <NTag type={row.status === '1' ? 'success' : 'warning'}>{label}</NTag>
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
@ -118,10 +123,10 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
|
||||
进入排行详情
|
||||
</NButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const {
|
||||
drawerVisible,
|
||||
@ -130,16 +135,14 @@ const {
|
||||
handleEdit,
|
||||
checkedRowKeys,
|
||||
onBatchDeleted,
|
||||
} = useTableOperate(data, 'id', getData);
|
||||
|
||||
|
||||
} = useTableOperate(data, 'id', getData)
|
||||
|
||||
function edit(id: number) {
|
||||
router.push({ name: 'rank_rank-detail', query: { id } });
|
||||
router.push({ name: 'rank_rank-detail', query: { id } })
|
||||
}
|
||||
|
||||
function handleBatchPublish() {
|
||||
window.$message?.success('批量发布成功');
|
||||
window.$message?.success('批量发布成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -150,8 +153,12 @@ function handleBatchPublish() {
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-16px font-bold">{{ rankTitle }}</div>
|
||||
<div class="text-12px text-gray-500 mt-4px">您可以对所有比赛场次的最终积分进行核对、修正,并正式发布结果。</div>
|
||||
<div class="text-16px font-bold">
|
||||
{{ rankTitle }}
|
||||
</div>
|
||||
<div class="mt-4px text-12px text-gray-500">
|
||||
您可以对所有比赛场次的最终积分进行核对、修正,并正式发布结果。
|
||||
</div>
|
||||
</div>
|
||||
<NButton type="primary" ghost class="ml-auto" @click="handleBatchPublish">
|
||||
<template #icon>
|
||||
@ -165,9 +172,11 @@ function handleBatchPublish() {
|
||||
<TableHeaderOperation v-model:columns="columnChecks" :disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading" @add="handleAdd" @delete="handleBatchDelete" @refresh="getData" />
|
||||
</template> -->
|
||||
<NDataTable v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" size="small" striped
|
||||
<NDataTable
|
||||
v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" size="small" striped
|
||||
:flex-height="!appStore.isMobile" :scroll-x="962" :loading="loading" remote :row-key="row => row.id"
|
||||
:pagination="mobilePagination" class="sm:h-full" />
|
||||
:pagination="mobilePagination" class="sm:h-full"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,53 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, toRaw } from 'vue';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { jsonClone } from '@sa/utils'
|
||||
import { computed, toRaw } from 'vue'
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'RankSearch'
|
||||
});
|
||||
name: 'RankSearch',
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
interface Emits {
|
||||
(e: 'search'): void;
|
||||
(e: 'search'): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm()
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true })
|
||||
|
||||
const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true });
|
||||
|
||||
type RuleKey = Extract<keyof Api.SystemManage.UserSearchParams, 'userEmail' | 'userPhone'>;
|
||||
type RuleKey = Extract<keyof Api.SystemManage.UserSearchParams, 'userEmail' | 'userPhone'>
|
||||
|
||||
const rules = computed<Record<RuleKey, App.Global.FormRule>>(() => {
|
||||
const { patternRules } = useFormRules();
|
||||
const { patternRules } = useFormRules()
|
||||
|
||||
return {
|
||||
userEmail: patternRules.email,
|
||||
userPhone: patternRules.phone
|
||||
};
|
||||
});
|
||||
userPhone: patternRules.phone,
|
||||
}
|
||||
})
|
||||
|
||||
const defaultModel = jsonClone(toRaw(model.value));
|
||||
const defaultModel = jsonClone(toRaw(model.value))
|
||||
|
||||
function resetModel() {
|
||||
Object.assign(model.value, defaultModel);
|
||||
Object.assign(model.value, defaultModel)
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
await restoreValidation();
|
||||
resetModel();
|
||||
await restoreValidation()
|
||||
resetModel()
|
||||
}
|
||||
|
||||
async function search() {
|
||||
await validate();
|
||||
emit('search');
|
||||
await validate()
|
||||
emit('search')
|
||||
}
|
||||
|
||||
const publishStatusOptions = [
|
||||
{ label: '待审核', value: '2' }, // Mapping to '2' (Warning)
|
||||
{ label: '已发布', value: '1' } // Mapping to '1' (Success)
|
||||
];
|
||||
{ label: '已发布', value: '1' }, // Mapping to '1' (Success)
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
<script setup lang="tsx">
|
||||
import { reactive } from 'vue';
|
||||
import { NButton, NPopconfirm, NTag } from 'naive-ui';
|
||||
import { fetchGetUserList } from '@/service/api';
|
||||
import { useAppStore } from '@/store/modules/app';
|
||||
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
|
||||
import TemplateOperateDrawer from './modules/template-operate-drawer.vue';
|
||||
import TemplateSearch from './modules/template-search.vue';
|
||||
import { NButton, NPopconfirm, NTag } from 'naive-ui'
|
||||
import { reactive } from 'vue'
|
||||
import { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table'
|
||||
import { fetchGetUserList } from '@/service/api'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import TemplateOperateDrawer from './modules/template-operate-drawer.vue'
|
||||
import TemplateSearch from './modules/template-search.vue'
|
||||
|
||||
const appStore = useAppStore();
|
||||
const appStore = useAppStore()
|
||||
|
||||
const searchParams: Api.SystemManage.UserSearchParams = reactive({
|
||||
current: 1,
|
||||
@ -17,48 +17,48 @@ const searchParams: Api.SystemManage.UserSearchParams = reactive({
|
||||
userGender: null,
|
||||
nickName: null,
|
||||
userPhone: null,
|
||||
userEmail: null
|
||||
});
|
||||
userEmail: null,
|
||||
})
|
||||
|
||||
const templateTitle = '模板管理';
|
||||
const templateTitle = '模板管理'
|
||||
|
||||
const competitionMap: Record<string, string> = {
|
||||
1: '第九届阅读之星大赛',
|
||||
2: '科普阅读大赛'
|
||||
};
|
||||
2: '科普阅读大赛',
|
||||
}
|
||||
|
||||
const statusMap: Record<string, string> = {
|
||||
1: '铺码成功',
|
||||
2: '未铺码'
|
||||
};
|
||||
2: '未铺码',
|
||||
}
|
||||
|
||||
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination } = useNaivePaginatedTable({
|
||||
api: () => fetchGetUserList(searchParams),
|
||||
transform: response => {
|
||||
const transformed = defaultTransform(response);
|
||||
transform: (response) => {
|
||||
const transformed = defaultTransform(response)
|
||||
transformed.data.forEach((item, index) => {
|
||||
item.nickName = `MD${String(index + 1).padStart(6, '0')}`;
|
||||
item.userPhone = '210mmx297mm';
|
||||
item.userGender = (item.userGender === '1' || item.userGender === '2') ? item.userGender : '1';
|
||||
});
|
||||
return transformed;
|
||||
item.nickName = `MD${String(index + 1).padStart(6, '0')}`
|
||||
item.userPhone = '210mmx297mm'
|
||||
item.userGender = (item.userGender === '1' || item.userGender === '2') ? item.userGender : '1'
|
||||
})
|
||||
return transformed
|
||||
},
|
||||
onPaginationParamsChange: params => {
|
||||
searchParams.current = params.page;
|
||||
searchParams.size = params.pageSize;
|
||||
onPaginationParamsChange: (params) => {
|
||||
searchParams.current = params.page
|
||||
searchParams.size = params.pageSize
|
||||
},
|
||||
columns: () => [
|
||||
{
|
||||
type: 'selection',
|
||||
align: 'center',
|
||||
width: 48
|
||||
width: 48,
|
||||
},
|
||||
{
|
||||
key: 'index',
|
||||
title: '序号',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
render: (_, index) => index + 1
|
||||
render: (_, index) => index + 1,
|
||||
},
|
||||
{
|
||||
key: 'userName',
|
||||
@ -74,10 +74,10 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
|
||||
key: 'userGender',
|
||||
title: '关联比赛',
|
||||
align: 'center',
|
||||
render: row => {
|
||||
const label = competitionMap[row.userGender as string] || '未知比赛';
|
||||
return <span>{label}</span>;
|
||||
}
|
||||
render: (row) => {
|
||||
const label = competitionMap[row.userGender as string] || '未知比赛'
|
||||
return <span>{label}</span>
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'userPhone',
|
||||
@ -89,13 +89,13 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
render: row => {
|
||||
render: (row) => {
|
||||
if (row.status === null) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const label = statusMap[row.status] || '未知';
|
||||
return <NTag type={row.status === '1' ? 'success' : 'warning'}>{label}</NTag>;
|
||||
}
|
||||
const label = statusMap[row.status] || '未知'
|
||||
return <NTag type={row.status === '1' ? 'success' : 'warning'}>{label}</NTag>
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
@ -126,14 +126,14 @@ const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagi
|
||||
<NButton type="error" ghost size="small">
|
||||
删除
|
||||
</NButton>
|
||||
)
|
||||
),
|
||||
}}
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
});
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const {
|
||||
drawerVisible,
|
||||
@ -143,26 +143,21 @@ const {
|
||||
handleEdit,
|
||||
checkedRowKeys,
|
||||
onBatchDeleted,
|
||||
onDeleted
|
||||
onDeleted,
|
||||
// closeDrawer
|
||||
} = useTableOperate(data, 'id', getData);
|
||||
} = useTableOperate(data, 'id', getData)
|
||||
|
||||
async function handleBatchDelete() {
|
||||
// request
|
||||
console.log(checkedRowKeys.value);
|
||||
|
||||
onBatchDeleted();
|
||||
onBatchDeleted()
|
||||
}
|
||||
|
||||
function handleDelete(id: number) {
|
||||
// request
|
||||
console.log(id);
|
||||
|
||||
onDeleted();
|
||||
onDeleted(id)
|
||||
}
|
||||
|
||||
function edit(id: number) {
|
||||
handleEdit(id);
|
||||
handleEdit(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -171,14 +166,20 @@ function edit(id: number) {
|
||||
<TemplateSearch v-model:model="searchParams" @search="getDataByPage" />
|
||||
<NCard :title="templateTitle" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
|
||||
<template #header-extra>
|
||||
<TableHeaderOperation v-model:columns="columnChecks" :disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading" @add="handleAdd" @delete="handleBatchDelete" @refresh="getData" />
|
||||
<TableHeaderOperation
|
||||
v-model:columns="columnChecks" :disabled-delete="checkedRowKeys.length === 0"
|
||||
:loading="loading" @add="handleAdd" @delete="handleBatchDelete" @refresh="getData"
|
||||
/>
|
||||
</template>
|
||||
<NDataTable v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" size="small" striped
|
||||
<NDataTable
|
||||
v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" size="small" striped
|
||||
:flex-height="!appStore.isMobile" :scroll-x="962" :loading="loading" remote :row-key="row => row.id"
|
||||
:pagination="mobilePagination" class="sm:h-full" />
|
||||
<TemplateOperateDrawer v-model:visible="drawerVisible" :operate-type="operateType" :row-data="editingData"
|
||||
@submitted="getDataByPage" />
|
||||
:pagination="mobilePagination" class="sm:h-full"
|
||||
/>
|
||||
<TemplateOperateDrawer
|
||||
v-model:visible="drawerVisible" :operate-type="operateType" :row-data="editingData"
|
||||
@submitted="getDataByPage"
|
||||
/>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1,47 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
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'
|
||||
});
|
||||
name: 'TemplateOperateDrawer',
|
||||
})
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
interface Props {
|
||||
operateType: NaiveUI.TableOperateType;
|
||||
rowData?: Api.SystemManage.User | null;
|
||||
operateType: NaiveUI.TableOperateType
|
||||
rowData?: Api.SystemManage.User | null
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
interface Emits {
|
||||
(e: 'submitted'): void;
|
||||
(e: 'submitted'): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
default: false,
|
||||
})
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
const { defaultRequiredRule } = useFormRules();
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm()
|
||||
const { defaultRequiredRule } = useFormRules()
|
||||
|
||||
const title = computed(() => {
|
||||
const titles: Record<NaiveUI.TableOperateType, string> = {
|
||||
add: '新增模板',
|
||||
edit: '编辑模板'
|
||||
};
|
||||
return titles[props.operateType];
|
||||
});
|
||||
edit: '编辑模板',
|
||||
}
|
||||
return titles[props.operateType]
|
||||
})
|
||||
|
||||
type Model = Pick<
|
||||
Api.SystemManage.User,
|
||||
'userName' | 'userGender' | 'nickName' | 'userPhone' | 'userEmail' | 'userRoles' | 'status'
|
||||
>;
|
||||
>
|
||||
|
||||
const model = ref(createDefaultModel());
|
||||
const model = ref(createDefaultModel())
|
||||
|
||||
function createDefaultModel(): Model {
|
||||
return {
|
||||
@ -51,53 +51,53 @@ function createDefaultModel(): Model {
|
||||
userPhone: '',
|
||||
userEmail: '',
|
||||
userRoles: [],
|
||||
status: null
|
||||
};
|
||||
status: null,
|
||||
}
|
||||
}
|
||||
|
||||
type RuleKey = Extract<keyof Model, 'userName' | 'status'>;
|
||||
type RuleKey = Extract<keyof Model, 'userName' | 'status'>
|
||||
|
||||
const rules: Record<RuleKey, App.Global.FormRule> = {
|
||||
userName: defaultRequiredRule,
|
||||
status: defaultRequiredRule
|
||||
};
|
||||
status: defaultRequiredRule,
|
||||
}
|
||||
|
||||
function handleInitModel() {
|
||||
model.value = createDefaultModel();
|
||||
model.value = createDefaultModel()
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
Object.assign(model.value, jsonClone(props.rowData));
|
||||
Object.assign(model.value, jsonClone(props.rowData))
|
||||
}
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
visible.value = false;
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
await validate()
|
||||
// request
|
||||
window.$message?.success('更新成功');
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
window.$message?.success('更新成功')
|
||||
closeDrawer()
|
||||
emit('submitted')
|
||||
}
|
||||
|
||||
watch(visible, () => {
|
||||
if (visible.value) {
|
||||
handleInitModel();
|
||||
restoreValidation();
|
||||
handleInitModel()
|
||||
restoreValidation()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const competitionOptions = [
|
||||
{ label: '第九届阅读之星大赛', value: '1' },
|
||||
{ label: '科普阅读大赛', value: '2' }
|
||||
];
|
||||
{ label: '科普阅读大赛', value: '2' },
|
||||
]
|
||||
|
||||
const sizeOptions = [
|
||||
{ label: '210mmx297mm', value: '1' },
|
||||
{ label: 'A3', value: '2' }
|
||||
];
|
||||
{ label: 'A3', value: '2' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -132,8 +132,12 @@ const sizeOptions = [
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<NSpace :size="16">
|
||||
<NButton @click="closeDrawer">取消</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">确认</NButton>
|
||||
<NButton @click="closeDrawer">
|
||||
取消
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">
|
||||
确认
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
|
||||
@ -1,59 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, toRaw } from 'vue';
|
||||
import { jsonClone } from '@sa/utils';
|
||||
import { enableStatusOptions } from '@/constants/business';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { jsonClone } from '@sa/utils'
|
||||
import { computed, toRaw } from 'vue'
|
||||
import { enableStatusOptions } from '@/constants/business'
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form'
|
||||
|
||||
defineOptions({
|
||||
name: 'TemplateSearch'
|
||||
});
|
||||
name: 'TemplateSearch',
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
interface Emits {
|
||||
(e: 'search'): void;
|
||||
(e: 'search'): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm()
|
||||
|
||||
const { formRef, validate, restoreValidation } = useNaiveForm();
|
||||
const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true })
|
||||
|
||||
const model = defineModel<Api.SystemManage.UserSearchParams>('model', { required: true });
|
||||
|
||||
type RuleKey = Extract<keyof Api.SystemManage.UserSearchParams, 'userEmail' | 'userPhone'>;
|
||||
type RuleKey = Extract<keyof Api.SystemManage.UserSearchParams, 'userEmail' | 'userPhone'>
|
||||
|
||||
const rules = computed<Record<RuleKey, App.Global.FormRule>>(() => {
|
||||
const { patternRules } = useFormRules();
|
||||
const { patternRules } = useFormRules()
|
||||
|
||||
return {
|
||||
userEmail: patternRules.email,
|
||||
userPhone: patternRules.phone
|
||||
};
|
||||
});
|
||||
userPhone: patternRules.phone,
|
||||
}
|
||||
})
|
||||
|
||||
const defaultModel = jsonClone(toRaw(model.value));
|
||||
const defaultModel = jsonClone(toRaw(model.value))
|
||||
|
||||
function resetModel() {
|
||||
Object.assign(model.value, defaultModel);
|
||||
Object.assign(model.value, defaultModel)
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
await restoreValidation();
|
||||
resetModel();
|
||||
await restoreValidation()
|
||||
resetModel()
|
||||
}
|
||||
|
||||
async function search() {
|
||||
await validate();
|
||||
emit('search');
|
||||
await validate()
|
||||
emit('search')
|
||||
}
|
||||
|
||||
const competitionOptions = [
|
||||
{ label: '第九届阅读之星大赛', value: '1' },
|
||||
{ label: '科普阅读大赛', value: '2' }
|
||||
];
|
||||
{ label: '科普阅读大赛', value: '2' },
|
||||
]
|
||||
|
||||
const sizeOptions = [
|
||||
{ label: '210mmx297mm', value: '1' },
|
||||
{ label: 'A3', value: '2' }
|
||||
];
|
||||
{ label: 'A3', value: '2' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -66,8 +66,10 @@ const sizeOptions = [
|
||||
<NInput v-model:value="model.userName" placeholder="请输入名称" />
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" label="关联比赛" path="userGender" class="pr-24px">
|
||||
<NSelect v-model:value="model.userGender" placeholder="请选择" :options="competitionOptions as any"
|
||||
clearable />
|
||||
<NSelect
|
||||
v-model:value="model.userGender" placeholder="请选择" :options="competitionOptions as any"
|
||||
clearable
|
||||
/>
|
||||
</NFormItemGi>
|
||||
<NFormItemGi span="24 s:12 m:6" label="状态" path="userStatus" class="pr-24px">
|
||||
<NSelect v-model:value="model.status" placeholder="请选择" :options="enableStatusOptions as any" clearable />
|
||||
|
||||
Reference in New Issue
Block a user