feat: 赛事1.09

This commit is contained in:
cst
2026-11-06 08:59:31 +08:00
parent ae960df897
commit 727778e675
14 changed files with 862 additions and 7 deletions
+5
View File
@@ -52,6 +52,11 @@ system.roles.add # 角色权限-新增(筛选栏中的"新增
system.roles.edit # 角色权限-编辑(表格操作列中的"编辑"按钮) system.roles.edit # 角色权限-编辑(表格操作列中的"编辑"按钮)
system.roles.delete # 角色权限-删除(表格操作列中的"删除"按钮,红色危险样式) system.roles.delete # 角色权限-删除(表格操作列中的"删除"按钮,红色危险样式)
system.notices.view # 公告管理-查看(页面默认查看权限)
system.notices.add # 公告管理-新增(筛选栏中的"新增"按钮)
system.notices.edit # 公告管理-编辑(表格操作列中的"修改"按钮)
system.notices.delete # 公告管理-删除(表格操作列中的"删除"按钮,红色危险样式)
system.logs.view # 操作日志-查看(页面默认查看权限,无操作列按钮) system.logs.view # 操作日志-查看(页面默认查看权限,无操作列按钮)
system.params.view # 参数配置-查看 system.params.view # 参数配置-查看
+54
View File
@@ -0,0 +1,54 @@
/**
* 公告管理 API
*/
import { get, post } from '@/utils/request';
import type {
NoticeListQueryParams,
NoticeSaveParams,
NoticeVO,
PageData,
ApiResult,
} from './types';
export type {
NoticeType,
NoticeVO,
NoticeListQueryParams,
NoticeSaveParams,
PageData,
ApiResult,
} from './types';
// ============================================================
// URL 常量
// ============================================================
const ANNOUNCEMENT_PAGE = '/admin/sys/announcement/page';
const ANNOUNCEMENT_SAVE = '/admin/sys/announcement/save';
const ANNOUNCEMENT_UPDATE = '/admin/sys/announcement/update';
const ANNOUNCEMENT_DELETE = '/admin/sys/announcement/delete';
// ============================================================
// API 函数
// ============================================================
/** 公告列表分页 — GET /admin/sys/announcement/page */
export function getNoticeList(
params: NoticeListQueryParams,
): Promise<ApiResult<PageData<NoticeVO>>> {
return get(ANNOUNCEMENT_PAGE, params as Record<string, any>);
}
/** 新增公告 — POST /admin/sys/announcement/save */
export function saveNotice(params: NoticeSaveParams): Promise<ApiResult<null>> {
return post(ANNOUNCEMENT_SAVE, params);
}
/** 修改公告 — POST /admin/sys/announcement/update */
export function updateNotice(params: NoticeSaveParams): Promise<ApiResult<null>> {
return post(ANNOUNCEMENT_UPDATE, params);
}
/** 删除公告(物理删除,单个)— POST /admin/sys/announcement/delete?id=xxx */
export function deleteNotice(id: number): Promise<ApiResult<null>> {
return post(ANNOUNCEMENT_DELETE, undefined, { id: String(id) });
}
+60
View File
@@ -0,0 +1,60 @@
/**
* 公告管理 - 类型定义
*/
/** 公告类型:1=首页公告提示,2=产品更新公告 */
export type NoticeType = 1 | 2;
/** 公告 VO */
export interface NoticeVO {
/** 公告 ID */
id: number;
/** 标题(最多 30 字) */
title: string;
/** 公告类型:1=首页公告提示,2=产品更新公告 */
type: number;
/** 公告类型名称 */
typeName: string;
/** 生效开始时间(YYYY-MM-DD HH:mm:ss) */
startTime: string;
/** 生效结束时间(YYYY-MM-DD HH:mm:ss) */
endTime: string;
/** 公告内容(纯文本,最多 500 字) */
context: string;
/** 创建时间 */
createDate: string;
}
/** 公告列表分页查询参数 */
export interface NoticeListQueryParams {
page: string;
limit: string;
/** 标题,模糊搜索 */
title?: string;
/** 公告类型 */
type?: number;
}
/** 新增/修改公告参数(id:新增不传或为 0,修改必传) */
export interface NoticeSaveParams {
id?: number;
title: string;
type: number;
startTime: string;
endTime: string;
context: string;
}
/** 分页数据结构 */
export interface PageData<T> {
total: number;
list: T[];
exData?: Record<string, any>;
}
/** 接口通用返回结构 */
export interface ApiResult<T> {
code: number;
msg: string;
data: T;
}
+11
View File
@@ -164,6 +164,17 @@ export const FALLBACK_MENU_NODES: MenuNode[] = [
icon: 'AiOutlineSafetyCertificate', icon: 'AiOutlineSafetyCertificate',
permission: 'system.roles.view', permission: 'system.roles.view',
}, },
{
// 公告管理:后端 /op/permission 尚未下发 system.notices.view 权限码,
// 暂不挂 permission(否则整条菜单会被权限过滤掉);后端配置后可在下方补回
// permission: 'system.notices.view',
id: 'system_notices',
name: '公告管理',
path: 'system/notices',
component: 'system/notices',
componentName: 'SystemNotices',
icon: 'AiOutlineSound',
},
{ {
id: 'system_params', id: 'system_params',
name: '参数配置', name: '参数配置',
+5 -2
View File
@@ -223,11 +223,14 @@ export default defineComponent({
</Form.Item> </Form.Item>
<Form.Item label="状态" name="status"> <Form.Item label="状态" name="status">
<Select <Select
mode="multiple"
value={filterForm.status} value={filterForm.status}
options={EVENT_STATUS_OPTIONS as any} options={EVENT_STATUS_OPTIONS as any}
style={{ width: '150px' }} style={{ width: '240px' }}
placeholder="全部"
maxTagCount={2}
allowClear allowClear
onUpdate:value={(val: any) => (filterForm.status = val || '')} onUpdate:value={(val: any) => (filterForm.status = val || [])}
/> />
</Form.Item> </Form.Item>
<Form.Item label="公开活动" name="isPublic"> <Form.Item label="公开活动" name="isPublic">
+5 -4
View File
@@ -10,7 +10,8 @@ export const FILTER_DEFAULTS = {
startDateRange: null as [string, string] | null, startDateRange: null as [string, string] | null,
name: '', name: '',
createName: '', createName: '',
status: '', // 状态多选:默认选中「报名中(0)」「进行中(1)」
status: ['0', '1'] as string[],
region: [] as string[], region: [] as string[],
isPublic: '', isPublic: '',
idCardNo: '', idCardNo: '',
@@ -28,7 +29,8 @@ export type FieldMapping = [
export const FIELD_MAPPINGS: FieldMapping[] = [ export const FIELD_MAPPINGS: FieldMapping[] = [
['name', 'name', (v) => v?.trim?.() ?? v], ['name', 'name', (v) => v?.trim?.() ?? v],
['createName', 'createName', (v) => v?.trim?.() ?? v], ['createName', 'createName', (v) => v?.trim?.() ?? v],
['status', 'status'], // 多选数组拼成逗号串传给后端,如 "0,1"
['status', 'status', (v) => (Array.isArray(v) ? v.join(',') : v)],
['isPublic', 'isPublic'], ['isPublic', 'isPublic'],
['idCardNo', 'idCardNo'], ['idCardNo', 'idCardNo'],
['idCardImg', 'idCardImg'], ['idCardImg', 'idCardImg'],
@@ -41,9 +43,8 @@ export const FIELD_MAPPINGS: FieldMapping[] = [
['scEnd', 'scEnd'], ['scEnd', 'scEnd'],
]; ];
/** 赛事状态选项(value 对应 API status) */ /** 赛事状态选项(多选,value 对应 API status;不选即全部) */
export const EVENT_STATUS_OPTIONS = [ export const EVENT_STATUS_OPTIONS = [
{ value: '', label: '全部' },
{ value: '0', label: '报名中' }, { value: '0', label: '报名中' },
{ value: '1', label: '进行中' }, { value: '1', label: '进行中' },
{ value: '2', label: '已结束' }, { value: '2', label: '已结束' },
@@ -88,7 +88,7 @@ export function useEventListModel() {
!!f.startDateRange || !!f.startDateRange ||
!!debouncedName.value?.trim() || !!debouncedName.value?.trim() ||
!!debouncedCreator.value?.trim() || !!debouncedCreator.value?.trim() ||
!!f.status || (Array.isArray(f.status) ? f.status.length > 0 : !!f.status) ||
f.region.length > 0 || f.region.length > 0 ||
!!f.isPublic || !!f.isPublic ||
!!f.idCardNo || !!f.idCardNo ||
@@ -0,0 +1,18 @@
.noticeFormModalMain {
.form {
:global {
.ant-form-item {
margin-bottom: 16px;
}
}
}
.footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
margin-top: 8px;
}
}
@@ -0,0 +1,216 @@
import { defineComponent, ref, reactive, nextTick } from 'vue';
import { useEffect } from '@/hooks';
import { Modal, Form, Input, Select, DatePicker, Button, message } from 'ant-design-vue';
import dayjs, { type Dayjs } from 'dayjs';
import { NOTICE_TYPE_OPTIONS } from '../model/useNoticeModel';
import { getNoticeList, type NoticeVO } from '../model/services';
import {
TITLE_MAX,
CONTENT_MAX,
SUBMIT_TIME_FORMAT,
typeRules,
timeRangeRules,
titleRules,
contextRules,
isTimeRangeConflict,
} from '../model/form';
import styles from './NoticeFormModal.module.less';
const { RangePicker } = DatePicker;
const { TextArea } = Input;
interface NoticeFormModalProps {
visible: boolean;
record: NoticeVO | null;
submitting?: boolean;
onClose: () => void;
onSave: (formData: any) => void;
}
/** 默认表单数据 */
const getDefaultForm = () => ({
type: 1 as number,
timeRange: null as [Dayjs, Dayjs] | null,
title: '',
context: '',
});
export default defineComponent({
name: 'NoticeFormModal',
props: {
visible: { type: Boolean, default: false },
record: { type: Object as () => NoticeVO | null, default: null },
submitting: { type: Boolean, default: false },
onClose: { type: Function, required: true },
onSave: { type: Function, required: true },
},
setup(props: NoticeFormModalProps) {
const formRef = ref<any>();
const formData = reactive(getDefaultForm());
/** 生效时间交叉校验中 */
const timeRangeChecking = ref(false);
/** 根据 record 初始化表单 */
const initFormFromRecord = (record: NoticeVO | null) => {
if (!record) {
Object.assign(formData, getDefaultForm());
return;
}
const startTime = record.startTime ? dayjs(record.startTime) : null;
const endTime = record.endTime ? dayjs(record.endTime) : null;
Object.assign(formData, {
type: record.type || 1,
timeRange: startTime?.isValid() && endTime?.isValid() ? [startTime, endTime] : null,
title: record.title || '',
context: record.context || '',
});
};
/** 监听 visible 变化重置表单 */
useEffect(() => {
if (props.visible) {
initFormFromRecord(props.record);
nextTick().then(() => formRef.value?.clearValidate());
}
}, [() => props.visible]);
/**
* 生效时间交叉校验:同一类型下,时间区间不允许与已存在公告交叉
* 校验通过返回 true
*/
const validateTimeRange = async (startTime: Dayjs, endTime: Dayjs): Promise<boolean> => {
timeRangeChecking.value = true;
try {
const res = await getNoticeList({ page: '1', limit: '999', type: formData.type });
if (res.code != 200) return true;
const editingId = props.record ? String(props.record.id) : '';
const conflictNotice = (res.data?.list || []).find(
(item) =>
String(item.id) !== editingId &&
isTimeRangeConflict(startTime, endTime, item.startTime, item.endTime),
);
if (conflictNotice) {
message.error(`生效时间与公告「${conflictNotice.title}」存在交叉,请重新选择`);
return false;
}
return true;
} catch {
// 查询失败不阻塞保存,交由后端二次校验
return true;
} finally {
timeRangeChecking.value = false;
}
};
/** 提交 */
const handleSubmit = async () => {
try {
await formRef.value?.validate();
} catch {
return;
}
const [startTime, endTime] = formData.timeRange || [];
if (!startTime || !endTime) return;
const isTimeRangeValid = await validateTimeRange(startTime, endTime);
if (!isTimeRangeValid) return;
props.onSave({
type: formData.type,
title: formData.title.trim(),
context: formData.context.trim(),
startTime: startTime.format(SUBMIT_TIME_FORMAT),
endTime: endTime.format(SUBMIT_TIME_FORMAT),
});
};
return () => {
const isEdit = !!props.record;
return (
<Modal
title={isEdit ? '编辑公告' : '新增公告'}
visible={props.visible}
onCancel={props.onClose}
width={560}
destroyOnClose
footer={null}
centered
wrapClassName={styles.noticeFormModalMain}
class={styles.modal}
>
<Form ref={formRef} layout="vertical" model={formData} class={styles.form} requiredMark>
{/* 类型 */}
<Form.Item label="类型" name="type" rules={typeRules}>
<Select
value={formData.type}
options={NOTICE_TYPE_OPTIONS as any}
placeholder="请选择"
onUpdate:value={(val: any) => (formData.type = val as number)}
/>
</Form.Item>
{/* 生效时间 */}
<Form.Item label="生效时间" name="timeRange" rules={timeRangeRules}>
<RangePicker
showTime
format={SUBMIT_TIME_FORMAT}
value={formData.timeRange as any}
placeholder={['开始时间', '结束时间']}
style={{ width: '100%' }}
allowClear
onUpdate:value={(val: any) => {
formData.timeRange = (val as [Dayjs, Dayjs]) || null;
formRef.value?.clearValidate('timeRange');
}}
/>
</Form.Item>
{/* 标题 */}
<Form.Item label="标题" name="title" rules={titleRules}>
<Input
value={formData.title}
placeholder="请输入公告标题"
maxlength={TITLE_MAX}
showCount
allowClear
onUpdate:value={(val: any) => {
formData.title = val?.target?.value ?? val ?? '';
}}
/>
</Form.Item>
{/* 内容 */}
<Form.Item label="内容" name="context" rules={contextRules}>
<TextArea
value={formData.context}
placeholder="请输入公告内容"
maxlength={CONTENT_MAX}
showCount
rows={6}
onUpdate:value={(val: any) => {
formData.context = val?.target?.value ?? val ?? '';
}}
/>
</Form.Item>
</Form>
{/* 底部按钮 */}
<div class={styles.footer}>
<Button onClick={props.onClose}>取消</Button>
<Button
type="primary"
loading={props.submitting || timeRangeChecking.value}
onClick={handleSubmit}
>
确定
</Button>
</div>
</Modal>
);
};
},
});
+208
View File
@@ -0,0 +1,208 @@
import { defineComponent } from 'vue';
import { Button, Input, Table, Form, Space, Modal, Pagination } from 'ant-design-vue';
import type { ModalProps } from 'ant-design-vue';
import { useNoticeModel } from './model/useNoticeModel';
import { formatEffectTime } from './model/form';
import { useContainerSize } from '@/hooks';
import { usePermissionStore } from '@/stores/permissionStore';
import NoticeFormModal from './components/NoticeFormModal';
import pageStyles from '@/assets/styles/pageLayout.module.less';
/**
* bodyCell 渲染
*/
function renderBodyCell({
column,
text,
record,
onEdit,
onDelete,
permissions,
}: {
column: any;
text: any;
record: any;
onEdit: (record: any) => void;
onDelete: (record: any) => void;
permissions: {
canEdit: boolean;
canDelete: boolean;
};
}) {
const { canEdit, canDelete } = permissions;
return column.key === 'effectTime' ? (
// 生效时间:开始时间 至 结束时间
<span>{formatEffectTime(record.startTime, record.endTime)}</span>
) : column.key === 'action' ? (
!canEdit && !canDelete ? (
<span>--</span>
) : (
<Space>
{canEdit && (
<Button type="link" size="small" onClick={() => onEdit(record)}>
修改
</Button>
)}
{canDelete && (
<Button type="link" size="small" danger onClick={() => onDelete(record)}>
删除
</Button>
)}
</Space>
)
) : (
<span>{text ?? '--'}</span>
);
}
/**
* 删除公告二次确认
*/
function confirmDelete({ record, onOk }: { record: any; onOk: (record: any) => void }) {
Modal.confirm({
title: '删除确认',
class: 'deleteConfirmModal',
content: `确认要删除“${record.title}”吗?`,
okText: '确认',
cancelText: '取消',
okButtonProps: { danger: true } as ModalProps['okButtonProps'],
onOk: () => onOk(record),
});
}
/**
* 系统管理 - 公告管理
*/
export default defineComponent({
name: 'SystemNotices',
setup() {
const {
filterForm,
loading,
dataSource,
columns,
pagination,
formVisible,
editingRecord,
formSubmitting,
handleSearch,
handleReset,
handlePageChange,
handleAdd,
handleEdit,
handleCloseForm,
handleSave,
handleDelete,
} = useNoticeModel();
const { containerRef, height } = useContainerSize();
const { hasPermission, hasAnyPermission } = usePermissionStore();
// 公告模块的权限码后端尚未配置时(/op/permission 未下发),按钮默认放行,
// 待后端下发任意一个 system.notices.* 权限码后自动切换为按码控制
const noticePermsConfigured = hasAnyPermission([
'system.notices.view',
'system.notices.add',
'system.notices.edit',
'system.notices.delete',
]);
const canAdd = !noticePermsConfigured || hasPermission('system.notices.add');
const operationPermissions = {
canEdit: !noticePermsConfigured || hasPermission('system.notices.edit'),
canDelete: !noticePermsConfigured || hasPermission('system.notices.delete'),
};
const triggerDelete = (record: any) => confirmDelete({ record, onOk: handleDelete });
const tableColumns = [
...columns,
{
title: '操作',
key: 'action',
width: 140,
fixed: 'right' as const,
align: 'center' as const,
},
];
return () => (
<div class={pageStyles.containerMain}>
{/* ===== 筛选区 ===== */}
<div class={pageStyles.filter}>
<Form style={{ rowGap: '10px' }} layout="inline" model={filterForm}>
<Form.Item label="标题" name="title">
<Input
value={filterForm.title}
placeholder="请输入"
style={{ width: '200px' }}
allowClear
onUpdate:value={(val: any) => (filterForm.title = val?.target?.value ?? val ?? '')}
onPressEnter={handleSearch}
/>
</Form.Item>
<Form.Item>
<Space>
<Button onClick={handleReset}>重置</Button>
<Button type="primary" onClick={handleSearch} loading={loading.value}>
查询
</Button>
{canAdd && (
<Button type="primary" onClick={handleAdd}>
新增
</Button>
)}
</Space>
</Form.Item>
</Form>
</div>
{/* ===== 表格区 ===== */}
<div class={pageStyles.table}>
<div ref={containerRef} class={pageStyles.tableBody}>
<Table
columns={tableColumns}
dataSource={dataSource.value}
loading={loading.value}
scroll={{ x: 'max-content', y: height.value }}
pagination={false}
>
{{
bodyCell: (args: any) =>
renderBodyCell({
...args,
onEdit: handleEdit,
onDelete: triggerDelete,
permissions: operationPermissions,
}),
}}
</Table>
</div>
{/* 分页 */}
<div class={pageStyles.pagination}>
<Pagination
current={(pagination as any).current.value}
pageSize={(pagination as any).pageSize.value}
total={(pagination as any).total.value}
showSizeChanger
showTotal={(total: number) => `共 ${total} 条`}
onChange={handlePageChange}
/>
</div>
</div>
{/* ===== 新增/编辑公告弹窗 ===== */}
{formVisible.value && (
<NoticeFormModal
visible={formVisible.value}
record={editingRecord.value}
submitting={formSubmitting.value}
onClose={handleCloseForm}
onSave={handleSave}
/>
)}
</div>
);
},
});
+68
View File
@@ -0,0 +1,68 @@
/**
* 公告管理 - 表单校验规则 & 时间处理工具
*/
import dayjs, { type Dayjs } from 'dayjs';
/** 标题最多字数 */
export const TITLE_MAX = 30;
/** 内容最多字数 */
export const CONTENT_MAX = 500;
/** 时间展示格式(精确到分钟) */
export const DISPLAY_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
/** 提交接口的时间格式 */
export const SUBMIT_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
/** 公告类型校验 */
export const typeRules = [
{ required: true, message: '请选择公告类型', trigger: 'change' as const },
];
/** 生效时间校验(开始 + 结束) */
export const timeRangeRules = [
{
required: true,
type: 'array' as const,
min: 2,
message: '请选择生效时间',
trigger: 'change' as const,
},
];
/** 标题校验 */
export const titleRules = [
{ required: true, whitespace: true, message: '请输入公告标题', trigger: 'blur' as const },
{ max: TITLE_MAX, message: `最多可输入${TITLE_MAX}字`, trigger: 'blur' as const },
];
/** 内容校验 */
export const contextRules = [
{ required: true, whitespace: true, message: '请输入公告内容', trigger: 'blur' as const },
{ max: CONTENT_MAX, message: `最多可输入${CONTENT_MAX}字`, trigger: 'blur' as const },
];
/** 将时间字符串格式化为展示格式(解析失败时原样返回) */
export function formatDisplayTime(value?: string): string {
if (!value) return '--';
const parsedTime = dayjs(value);
return parsedTime.isValid() ? parsedTime.format(DISPLAY_TIME_FORMAT) : value;
}
/** 列表「生效时间」列:开始时间 至 结束时间 */
export function formatEffectTime(startTime?: string, endTime?: string): string {
if (!startTime && !endTime) return '--';
return `${formatDisplayTime(startTime)} 至 ${formatDisplayTime(endTime)}`;
}
/** 判断两个时间区间是否交叉(首尾相接不算交叉) */
export function isTimeRangeConflict(
start: Dayjs,
end: Dayjs,
otherStart?: string,
otherEnd?: string,
): boolean {
const otherStartTime = dayjs(otherStart);
const otherEndTime = dayjs(otherEnd);
if (!otherStartTime.isValid() || !otherEndTime.isValid()) return false;
return start.isBefore(otherEndTime) && otherStartTime.isBefore(end);
}
@@ -0,0 +1,13 @@
/**
* 公告管理 - 接口服务
* 该页面使用的所有接口集中管理(统一收敛到 @/api/notices)
*/
export { getNoticeList, saveNotice, updateNotice, deleteNotice } from '@/api/notices';
export type {
NoticeVO,
NoticeListQueryParams,
NoticeSaveParams,
PageData,
ApiResult,
} from '@/api/notices';
@@ -0,0 +1,196 @@
import { computed, reactive, nextTick } from 'vue';
import { message } from 'ant-design-vue';
import { useState, useThrottleFn } from '@/hooks';
import { useRequest } from '@/hooks/useRequest';
import { useEffect } from '@/hooks/useEffect';
import { usePagination, syncPaginationTotal } from '@/hooks/usePagination';
import {
getNoticeList,
saveNotice,
updateNotice,
deleteNotice,
type NoticeVO,
type NoticeListQueryParams,
type PageData,
type ApiResult,
} from './services';
// ============================================================
// 常量
// ============================================================
/** 公告类型选项(筛选 / 表单共用) */
export const NOTICE_TYPE_OPTIONS = [
{ value: 1, label: '首页公告提示' },
{ value: 2, label: '产品更新公告' },
] as const;
/** 公告类型文案映射(兜底,后端 typeName 为空时使用) */
export const NOTICE_TYPE_MAP: Record<number, string> = {
1: '首页公告提示',
2: '产品更新公告',
};
// ============================================================
// Model
// ============================================================
/**
* 公告管理页数据模型
*/
export function useNoticeModel() {
// ===== 筛选条件 =====
const filterForm = reactive({
title: '', // 公告标题
});
// ===== 分页 =====
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
// ===== 构建 API 查询参数 =====
const buildQueryParams = (): NoticeListQueryParams => {
const params: NoticeListQueryParams = {
page: String((pagination as any).current.value),
limit: String((pagination as any).pageSize.value),
};
if (filterForm.title.trim()) params.title = filterForm.title.trim();
return params;
};
// ===== 数据请求 =====
const {
data,
loading,
run: fetchList,
} = useRequest<ApiResult<PageData<NoticeVO>>>(() => getNoticeList(buildQueryParams()), {
refreshDeps: [],
formatResult: (res) => res,
});
const dataSource = computed(() => {
const res = data.value;
return res ? (res as any).data?.list || [] : [];
});
useEffect(() => {
syncPaginationTotal(pagination, (data.value as any)?.data?.total);
}, [data]);
// ===== 弹窗状态 =====
const [formVisible, setFormVisible] = useState<boolean>(false);
const [editingRecord, setEditingRecord] = useState<NoticeVO | null>(null);
// ===== 提交态 =====
const [formSubmitting, setFormSubmitting] = useState<boolean>(false);
// ===== 表格列配置 =====
const columns = [
{ title: '标题', dataIndex: 'title', key: 'title' },
{ title: '公告类型', dataIndex: 'typeName', key: 'typeName', width: 200 },
{ title: '生效时间', key: 'effectTime', width: 340 },
{ title: '创建时间', dataIndex: 'createDate', key: 'createDate', width: 200 },
];
// ===== 计算属性 =====
const hasFilter = computed(() => filterForm.title.trim() !== '');
const isEdit = computed(() => editingRecord.value !== null);
// ===== 方法 =====
const handleSearch = () => {
pagination.reset();
fetchList();
};
/** 重置(重置后自动查询) */
const handleReset = useThrottleFn(async () => {
filterForm.title = '';
pagination.reset();
await fetchList();
}, 500);
const handlePageChange = (page: number, pageSize: number) => {
pagination.setCurrent(page);
if (pageSize !== (pagination as any).pageSize.value) {
pagination.setPageSize(pageSize);
}
nextTick().then(() => fetchList());
};
/** 新增 */
const handleAdd = () => {
setEditingRecord(null);
setFormVisible(true);
};
/** 编辑 */
const handleEdit = (record: NoticeVO) => {
setEditingRecord(record);
setFormVisible(true);
};
/** 关闭表单弹窗 */
const handleCloseForm = () => {
setFormVisible(false);
setEditingRecord(null);
};
/** 提交(新增/编辑) */
const handleSave = useThrottleFn(async (formPayload: any) => {
if (formSubmitting.value) return;
setFormSubmitting(true);
try {
const res = isEdit.value
? await updateNotice({ id: editingRecord.value?.id, ...formPayload })
: await saveNotice(formPayload);
if (res.code == 200) {
message.success(isEdit.value ? '编辑成功' : '新增成功');
handleCloseForm();
fetchList();
} else {
message.error(res.msg || '操作失败');
}
} catch {
// 网络层已统一提示,不再重复 message.error
} finally {
setFormSubmitting(false);
}
}, 500);
/** 删除 */
const handleDelete = useThrottleFn(async (record: NoticeVO) => {
try {
const res = await deleteNotice(record.id);
if (res.code == 200) {
message.success('删除成功');
fetchList();
} else {
message.error(res.msg || '删除失败');
}
} catch {
// 网络层已统一提示,不再重复 message.error
}
}, 500);
return {
filterForm,
loading,
dataSource,
columns,
pagination,
hasFilter,
formVisible,
editingRecord,
isEdit,
formSubmitting,
handleSearch,
handleReset,
handlePageChange,
handleAdd,
handleEdit,
handleCloseForm,
handleSave,
handleDelete,
};
}
+2
View File
@@ -10,6 +10,7 @@ import {
AiOutlineSafetyCertificate, AiOutlineSafetyCertificate,
AiOutlineSetting, AiOutlineSetting,
AiOutlineShoppingCart, AiOutlineShoppingCart,
AiOutlineSound,
AiOutlineTeam, AiOutlineTeam,
AiOutlineTransaction, AiOutlineTransaction,
AiOutlineTrophy, AiOutlineTrophy,
@@ -35,6 +36,7 @@ const ROUTE_ICON_MAP: Record<string, IconType> = {
AiOutlineUser, AiOutlineUser,
AiOutlineSafetyCertificate, AiOutlineSafetyCertificate,
AiOutlineAudit, AiOutlineAudit,
AiOutlineSound,
}; };
export type RouteIconName = keyof typeof ROUTE_ICON_MAP; export type RouteIconName = keyof typeof ROUTE_ICON_MAP;