feat: 处理 ignore 干扰问题 优化文件依赖
This commit is contained in:
+5
-1
@@ -1,5 +1,5 @@
|
||||
# Logs
|
||||
logs
|
||||
/logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
@@ -40,3 +40,7 @@ coverage
|
||||
.cache
|
||||
.temp
|
||||
.tmp
|
||||
|
||||
# Easy LESS 误生成的编译产物(本项目为 Vue PC 端,样式源文件为 .less,由 Vite 编译)
|
||||
*.wxss
|
||||
*.module.css
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 操作日志 API
|
||||
*/
|
||||
import { get } from '@/utils/request';
|
||||
import { USE_MOCK, MOCK_DELAY } from '@/config/mock';
|
||||
import { buildMockOperationLogPage } from '@/config/mock/operationLogs';
|
||||
import { buildMockEventOperationLogPage } from '@/config/mock/eventOperationLogs';
|
||||
import type {
|
||||
OperationLogQueryParams,
|
||||
SysOperationLogVO,
|
||||
TournamentAdminOperationPageVO,
|
||||
PageData,
|
||||
ApiResult,
|
||||
} from './types';
|
||||
|
||||
export type {
|
||||
SysOperationLogVO,
|
||||
TournamentAdminOperationPageVO,
|
||||
OperationLogQueryParams,
|
||||
PageData,
|
||||
ApiResult,
|
||||
} from './types';
|
||||
|
||||
const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||||
|
||||
function pickFilters(params: OperationLogQueryParams): OperationLogQueryParams {
|
||||
const { page: _page, limit: _limit, ...filters } = params;
|
||||
return filters;
|
||||
}
|
||||
|
||||
/** GET /admin/sys/operation/page — 系统操作日志分页 */
|
||||
export async function getOperationLogList(
|
||||
params: OperationLogQueryParams,
|
||||
): Promise<ApiResult<PageData<SysOperationLogVO>>> {
|
||||
if (USE_MOCK) {
|
||||
await delay(MOCK_DELAY);
|
||||
const page = Number(params.page) || 1;
|
||||
const limit = Number(params.limit) || 10;
|
||||
return {
|
||||
code: 200,
|
||||
msg: 'success',
|
||||
data: buildMockOperationLogPage(page, limit, pickFilters(params)),
|
||||
};
|
||||
}
|
||||
return get('/admin/sys/operation/page', params as Record<string, any>);
|
||||
}
|
||||
|
||||
/** GET /admin/tournament/operation/page — 赛事操作日志分页 */
|
||||
export async function getOperationLogs(
|
||||
params: OperationLogQueryParams,
|
||||
): Promise<ApiResult<PageData<TournamentAdminOperationPageVO>>> {
|
||||
if (USE_MOCK) {
|
||||
await delay(MOCK_DELAY);
|
||||
const page = Number(params.page) || 1;
|
||||
const limit = Number(params.limit) || 10;
|
||||
return {
|
||||
code: 200,
|
||||
msg: 'success',
|
||||
data: buildMockEventOperationLogPage(page, limit, pickFilters(params)),
|
||||
};
|
||||
}
|
||||
return get('/admin/tournament/operation/page', params as Record<string, any>);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 getOperationLogList */
|
||||
export const getOperationLogPage = getOperationLogList;
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* 操作日志 — 类型定义
|
||||
*/
|
||||
|
||||
/** 系统操作日志列表项(GET /admin/sys/operation/page) */
|
||||
export interface SysOperationLogVO {
|
||||
/** 操作类型:1=下架赛事、2=上架赛事、3=禁用用户、4=启用用户 */
|
||||
type: number;
|
||||
/** 操作人昵称 */
|
||||
nickname: string;
|
||||
/** 操作时间 */
|
||||
createDate: string;
|
||||
/** 操作内容 */
|
||||
content: string;
|
||||
}
|
||||
|
||||
/** 赛事操作日志列表项 */
|
||||
export interface TournamentAdminOperationPageVO {
|
||||
opName: string;
|
||||
source: string;
|
||||
nickname: string;
|
||||
phone: string;
|
||||
tournamentName: string;
|
||||
obj: string;
|
||||
content: string;
|
||||
createDate: string;
|
||||
}
|
||||
|
||||
/** 操作日志列表查询参数 */
|
||||
export interface OperationLogQueryParams {
|
||||
page?: string;
|
||||
limit?: string;
|
||||
/** 操作类型 */
|
||||
type?: string;
|
||||
/** 系统日志:操作人昵称 */
|
||||
nickname?: string;
|
||||
/** 赛事日志:操作来源 1=PC,2=小程序 */
|
||||
source?: string;
|
||||
/** 赛事日志:操作人昵称 */
|
||||
sourceNickname?: string;
|
||||
/** 赛事日志:操作人手机号 */
|
||||
sourcePhone?: string;
|
||||
/** 赛事日志:赛事名称 */
|
||||
tournamentName?: string;
|
||||
/** 开始日期 yyyy-MM-dd */
|
||||
dateBegin?: string;
|
||||
/** 结束日期 yyyy-MM-dd */
|
||||
dateEnd?: string;
|
||||
}
|
||||
|
||||
export interface PageData<T> {
|
||||
total: number;
|
||||
list: T[];
|
||||
}
|
||||
|
||||
export interface ApiResult<T> {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
@@ -1,13 +1,32 @@
|
||||
/**
|
||||
* 赛事操作日志假数据
|
||||
*
|
||||
* 类型内联,避免与 @/api/logs 循环引用。
|
||||
*/
|
||||
import type {
|
||||
OperationLogQueryParams,
|
||||
PageData,
|
||||
TournamentAdminOperationPageVO,
|
||||
} from '@/api/logs/types';
|
||||
interface MockTournamentAdminOperationPageVO {
|
||||
opName: string;
|
||||
source: string;
|
||||
nickname: string;
|
||||
phone: string;
|
||||
tournamentName: string;
|
||||
obj: string;
|
||||
content: string;
|
||||
createDate: string;
|
||||
}
|
||||
|
||||
const MOCK_EVENT_LOGS: TournamentAdminOperationPageVO[] = [
|
||||
interface MockEventOperationLogQueryParams {
|
||||
source?: string;
|
||||
sourceNickname?: string;
|
||||
sourcePhone?: string;
|
||||
tournamentName?: string;
|
||||
}
|
||||
|
||||
interface MockPageData<T> {
|
||||
total: number;
|
||||
list: T[];
|
||||
}
|
||||
|
||||
const MOCK_EVENT_LOGS: MockTournamentAdminOperationPageVO[] = [
|
||||
{
|
||||
opName: '创建赛事',
|
||||
source: 'PC',
|
||||
@@ -33,8 +52,8 @@ const MOCK_EVENT_LOGS: TournamentAdminOperationPageVO[] = [
|
||||
export function buildMockEventOperationLogPage(
|
||||
page: number,
|
||||
limit: number,
|
||||
params: OperationLogQueryParams,
|
||||
): PageData<TournamentAdminOperationPageVO> {
|
||||
params: MockEventOperationLogQueryParams,
|
||||
): MockPageData<MockTournamentAdminOperationPageVO> {
|
||||
let filtered = [...MOCK_EVENT_LOGS];
|
||||
|
||||
if (params.source) {
|
||||
|
||||
@@ -2,12 +2,28 @@
|
||||
* 操作日志假数据
|
||||
* 对应接口:GET /admin/sys/operation/page
|
||||
*
|
||||
* 支持按 type、nickname、dateBegin/dateEnd 前端筛选,
|
||||
* 模拟后端分页行为。
|
||||
* 类型内联,避免与 @/api/logs 循环引用。
|
||||
*/
|
||||
import type { SysOperationLogVO, OperationLogQueryParams, PageData } from '@/api/logs/types';
|
||||
interface MockSysOperationLogVO {
|
||||
type: number;
|
||||
nickname: string;
|
||||
createDate: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const MOCK_LOGS: SysOperationLogVO[] = [
|
||||
interface MockOperationLogQueryParams {
|
||||
type?: string;
|
||||
nickname?: string;
|
||||
dateBegin?: string;
|
||||
dateEnd?: string;
|
||||
}
|
||||
|
||||
interface MockPageData<T> {
|
||||
total: number;
|
||||
list: T[];
|
||||
}
|
||||
|
||||
const MOCK_LOGS: MockSysOperationLogVO[] = [
|
||||
{
|
||||
type: 1,
|
||||
nickname: '张三',
|
||||
@@ -133,7 +149,7 @@ const MOCK_LOGS: SysOperationLogVO[] = [
|
||||
/** 按日期范围过滤(dateBegin / dateEnd 格式 yyyy-MM-dd) */
|
||||
function inDateRange(dateStr: string, begin?: string, end?: string): boolean {
|
||||
if (!begin && !end) return true;
|
||||
const d = dateStr.slice(0, 10); // yyyy-MM-dd
|
||||
const d = dateStr.slice(0, 10);
|
||||
if (begin && d < begin) return false;
|
||||
if (end && d > end) return false;
|
||||
return true;
|
||||
@@ -142,21 +158,18 @@ function inDateRange(dateStr: string, begin?: string, end?: string): boolean {
|
||||
export function buildMockOperationLogPage(
|
||||
page: number,
|
||||
limit: number,
|
||||
params: OperationLogQueryParams,
|
||||
): PageData<SysOperationLogVO> {
|
||||
params: MockOperationLogQueryParams,
|
||||
): MockPageData<MockSysOperationLogVO> {
|
||||
let filtered = [...MOCK_LOGS];
|
||||
|
||||
// 按操作类型筛选(API 传 '1'/'2'/'3'/'4')
|
||||
if (params.type) {
|
||||
filtered = filtered.filter((item) => item.type === Number(params.type));
|
||||
}
|
||||
|
||||
// 按操作人昵称模糊筛选
|
||||
if (params.nickname) {
|
||||
filtered = filtered.filter((item) => item.nickname.includes(params.nickname!));
|
||||
}
|
||||
|
||||
// 按日期范围筛选
|
||||
filtered = filtered.filter((item) =>
|
||||
inDateRange(item.createDate, params.dateBegin, params.dateEnd),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 操作内容单元格:文字最多 2 行省略,末尾带"查看"链接
|
||||
:global {
|
||||
.log-content-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.log-content-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.log-content-view {
|
||||
flex-shrink: 0;
|
||||
color: #1677ff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
line-height: 1.5;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import { defineComponent, ref, onMounted, onUnmounted, nextTick } from 'vue';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Table,
|
||||
DatePicker,
|
||||
Form,
|
||||
Space,
|
||||
Select,
|
||||
Tooltip,
|
||||
Pagination,
|
||||
} from 'ant-design-vue';
|
||||
import {
|
||||
useLogModel,
|
||||
ACTION_TYPE_OPTIONS,
|
||||
ACTION_SOURCE_OPTIONS,
|
||||
ACTION_SOURCE_MAP,
|
||||
} from './model/useLogModel';
|
||||
import { useState, useContainerSize } from '@/hooks';
|
||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||
import './index.module.less';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
// ============================================================
|
||||
// LogContentCell: 操作内容单元格
|
||||
// ============================================================
|
||||
const LogContentCell = defineComponent({
|
||||
name: 'LogContentCell',
|
||||
props: { text: { type: String, required: true } },
|
||||
setup(props) {
|
||||
const textRef = ref<HTMLElement | null>(null);
|
||||
const [overflow, setOverflow] = useState(false);
|
||||
const checkOverflow = () => {
|
||||
const el = textRef.value;
|
||||
if (el) setOverflow(el.scrollHeight > el.clientHeight);
|
||||
};
|
||||
let observer: ResizeObserver | null = null;
|
||||
onMounted(() => {
|
||||
nextTick(checkOverflow);
|
||||
const el = textRef.value;
|
||||
if (el) {
|
||||
observer = new ResizeObserver(checkOverflow);
|
||||
observer.observe(el);
|
||||
}
|
||||
});
|
||||
onUnmounted(() => observer?.disconnect());
|
||||
return () => {
|
||||
const raw = props.text;
|
||||
return (
|
||||
<div class="log-content-cell">
|
||||
<div ref={textRef} class="log-content-text">
|
||||
{raw}
|
||||
</div>
|
||||
{overflow.value ? (
|
||||
<Tooltip title={raw} placement="topLeft">
|
||||
<span class="log-content-view">查看</span>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// bodyCell 渲染
|
||||
// ============================================================
|
||||
function renderBodyCell({ column, text }: { column: any; text: any }) {
|
||||
const value = text || '-';
|
||||
if (column.key === 'source') return <span>{ACTION_SOURCE_MAP[text] || value}</span>;
|
||||
return <span>{value}</span>;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EventLogs',
|
||||
setup() {
|
||||
const {
|
||||
filterForm,
|
||||
loading,
|
||||
dataSource,
|
||||
columns,
|
||||
pagination,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handlePageChange,
|
||||
} = useLogModel();
|
||||
const { containerRef, height } = useContainerSize({ headerOffset: 55 });
|
||||
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
});
|
||||
|
||||
return () => (
|
||||
<div class={pageStyles.containerMain}>
|
||||
<div class={pageStyles.filter}>
|
||||
<Form style={{ rowGap: '10px' }} layout="inline" model={filterForm}>
|
||||
<Form.Item label="操作时间" name="dateRange">
|
||||
<RangePicker
|
||||
value={filterForm.dateRange as any}
|
||||
format="YYYY-MM-DD"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
style={{ width: '280px' }}
|
||||
allowClear
|
||||
onUpdate:value={(val: any) => (filterForm.dateRange = val)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="操作类型" name="type">
|
||||
<Select
|
||||
value={filterForm.type}
|
||||
options={ACTION_TYPE_OPTIONS as any}
|
||||
style={{ width: '180px' }}
|
||||
allowClear
|
||||
placeholder="全部"
|
||||
onUpdate:value={(val: any) => (filterForm.type = val || '')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="操作来源" name="source">
|
||||
<Select
|
||||
value={filterForm.source}
|
||||
options={ACTION_SOURCE_OPTIONS as any}
|
||||
style={{ width: '120px' }}
|
||||
allowClear
|
||||
placeholder="全部"
|
||||
onUpdate:value={(val: any) => (filterForm.source = val || '')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="操作人昵称" name="sourceNickname">
|
||||
<Input
|
||||
value={filterForm.sourceNickname}
|
||||
placeholder="请输入"
|
||||
style={{ width: '160px' }}
|
||||
allowClear
|
||||
onUpdate:value={(val: any) =>
|
||||
(filterForm.sourceNickname = val.target?.value ?? val ?? '')
|
||||
}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="手机号" name="sourcePhone">
|
||||
<Input
|
||||
value={filterForm.sourcePhone}
|
||||
placeholder="请输入"
|
||||
style={{ width: '160px' }}
|
||||
allowClear
|
||||
onUpdate:value={(val: any) =>
|
||||
(filterForm.sourcePhone = val.target?.value ?? val ?? '')
|
||||
}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="赛事名称" name="tournamentName">
|
||||
<Input
|
||||
value={filterForm.tournamentName}
|
||||
placeholder="请输入"
|
||||
style={{ width: '200px' }}
|
||||
allowClear
|
||||
onUpdate:value={(val: any) =>
|
||||
(filterForm.tournamentName = val.target?.value ?? val ?? '')
|
||||
}
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button onClick={handleReset}>重置</Button>
|
||||
<Button type="primary" onClick={handleSearch} loading={loading.value}>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div class={pageStyles.table}>
|
||||
<div ref={containerRef} class={pageStyles.tableBody}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dataSource.value}
|
||||
loading={loading.value}
|
||||
scroll={{ x: 'max-content', y: height.value }}
|
||||
pagination={false}
|
||||
>
|
||||
{{
|
||||
bodyCell: (args: any) => {
|
||||
if (args.column.key === 'content') {
|
||||
const raw = args.text || '';
|
||||
return raw ? <LogContentCell text={raw} /> : <span>-</span>;
|
||||
}
|
||||
return renderBodyCell(args);
|
||||
},
|
||||
}}
|
||||
</Table>
|
||||
</div>
|
||||
<div class={pageStyles.pagination}>
|
||||
<Pagination
|
||||
current={pagination.value.current}
|
||||
pageSize={pagination.value.pageSize}
|
||||
total={pagination.value.total}
|
||||
showSizeChanger
|
||||
showTotal={(total: number) => `共 ${total} 条`}
|
||||
onChange={handlePageChange}
|
||||
onShowSizeChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
import { computed, reactive, toRef, Ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useState, useDebounce, useThrottleFn } from '@/hooks';
|
||||
import {
|
||||
getOperationLogs,
|
||||
type TournamentAdminOperationPageVO,
|
||||
type OperationLogQueryParams,
|
||||
} from '@/api/logs';
|
||||
|
||||
// ============================================================
|
||||
// 常量
|
||||
// ============================================================
|
||||
|
||||
/** 操作类型选项(TODO: API type 字段待定意,暂时留空) */
|
||||
export const ACTION_TYPE_OPTIONS = [{ value: '', label: '全部' }] as const;
|
||||
|
||||
/** 操作来源选项(value 对应 API source: 1=PC,2=小程序) */
|
||||
export const ACTION_SOURCE_OPTIONS = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: '1', label: 'PC' },
|
||||
{ value: '2', label: '小程序' },
|
||||
] as const;
|
||||
|
||||
/** 操作来源文案映射 */
|
||||
export const ACTION_SOURCE_MAP: Record<string, string> = {
|
||||
'1': 'PC',
|
||||
'2': '小程序',
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Model
|
||||
// ============================================================
|
||||
|
||||
export function useLogModel() {
|
||||
// ===== 筛选条件(key 名对齐 API 查询参数) =====
|
||||
const filterForm = reactive({
|
||||
dateRange: null as [string, string] | null,
|
||||
type: '',
|
||||
source: '',
|
||||
sourceNickname: '',
|
||||
sourcePhone: '',
|
||||
tournamentName: '',
|
||||
});
|
||||
|
||||
const { debouncedValue: debouncedNickname } = useDebounce(
|
||||
toRef(filterForm, 'sourceNickname') as Ref<string>,
|
||||
{ delay: 300 },
|
||||
);
|
||||
const { debouncedValue: debouncedPhone } = useDebounce(
|
||||
toRef(filterForm, 'sourcePhone') as Ref<string>,
|
||||
{ delay: 300 },
|
||||
);
|
||||
const { debouncedValue: debouncedTournamentName } = useDebounce(
|
||||
toRef(filterForm, 'tournamentName') as Ref<string>,
|
||||
{ delay: 300 },
|
||||
);
|
||||
|
||||
// ===== 表格状态 =====
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [dataSource, setDataSource] = useState<TournamentAdminOperationPageVO[]>([]);
|
||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
// ===== 表格列配置(dataIndex 对齐 TournamentAdminOperationPageVO) =====
|
||||
const columns = [
|
||||
{ title: '操作类型', dataIndex: 'opName', key: 'opName', width: 140 },
|
||||
{ title: '操作来源', dataIndex: 'source', key: 'source', width: 100 },
|
||||
{ title: '操作人昵称', dataIndex: 'nickname', key: 'nickname', width: 120 },
|
||||
{ title: '操作人手机号', dataIndex: 'phone', key: 'phone', width: 140 },
|
||||
{ title: '赛事名称', dataIndex: 'tournamentName', key: 'tournamentName', width: 200 },
|
||||
{ title: '操作对象', dataIndex: 'obj', key: 'obj', width: 240 },
|
||||
{ title: '操作内容', dataIndex: 'content', key: 'content', width: 360 },
|
||||
{ title: '操作时间', dataIndex: 'createDate', key: 'createDate', width: 170 },
|
||||
];
|
||||
|
||||
// ===== 构建 API 查询参数 =====
|
||||
const buildQueryParams = (): OperationLogQueryParams => {
|
||||
const params: OperationLogQueryParams = {
|
||||
page: String(pagination.value.current),
|
||||
limit: String(pagination.value.pageSize),
|
||||
};
|
||||
if (filterForm.dateRange) {
|
||||
params.dateBegin = filterForm.dateRange[0];
|
||||
params.dateEnd = filterForm.dateRange[1];
|
||||
}
|
||||
if (filterForm.type) params.type = filterForm.type;
|
||||
if (filterForm.source) params.source = filterForm.source;
|
||||
if (filterForm.sourceNickname.trim()) params.sourceNickname = filterForm.sourceNickname.trim();
|
||||
if (filterForm.sourcePhone.trim()) params.sourcePhone = filterForm.sourcePhone.trim();
|
||||
if (filterForm.tournamentName.trim()) params.tournamentName = filterForm.tournamentName.trim();
|
||||
return params;
|
||||
};
|
||||
|
||||
// ===== 计算属性 =====
|
||||
const hasFilter = computed(() => {
|
||||
return (
|
||||
filterForm.dateRange !== null ||
|
||||
filterForm.type !== '' ||
|
||||
filterForm.source !== '' ||
|
||||
debouncedNickname.value.trim() !== '' ||
|
||||
debouncedPhone.value.trim() !== '' ||
|
||||
debouncedTournamentName.value.trim() !== ''
|
||||
);
|
||||
});
|
||||
|
||||
// ===== 方法 =====
|
||||
|
||||
const handleSearch = useThrottleFn(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = buildQueryParams();
|
||||
console.log('操作日志查询参数:', params);
|
||||
const res = await getOperationLogs(params);
|
||||
if (res.code === 200) {
|
||||
setDataSource(res.data.list);
|
||||
setPagination({ ...pagination.value, total: res.data.total });
|
||||
} else {
|
||||
message.error(res.msg || '查询失败');
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('操作日志查询失败:', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const handleReset = useThrottleFn(() => {
|
||||
filterForm.dateRange = null;
|
||||
filterForm.type = '';
|
||||
filterForm.source = '';
|
||||
filterForm.sourceNickname = '';
|
||||
filterForm.sourcePhone = '';
|
||||
filterForm.tournamentName = '';
|
||||
setPagination({ current: 1, pageSize: 10, total: 0 });
|
||||
setDataSource([]);
|
||||
setTimeout(() => handleSearch(), 350);
|
||||
}, 500);
|
||||
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
setPagination({ ...pagination.value, current: page, pageSize });
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
return {
|
||||
filterForm,
|
||||
loading,
|
||||
dataSource,
|
||||
columns,
|
||||
pagination,
|
||||
hasFilter,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handlePageChange,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { defineComponent, onMounted } from 'vue';
|
||||
import { Button, Input, Table, DatePicker, Form, Space, Select, Pagination } from 'ant-design-vue';
|
||||
import { useLogModel, ACTION_TYPE_OPTIONS, ACTION_TYPE_MAP } from './model/useLogModel';
|
||||
import { useContainerSize } from '@/hooks';
|
||||
import pageStyles from '@/assets/styles/pageLayout.module.less';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
/**
|
||||
* bodyCell 渲染
|
||||
*/
|
||||
function renderBodyCell({ column, text }: { column: any; text: any }) {
|
||||
if (column.key === 'type') {
|
||||
return <span>{ACTION_TYPE_MAP[text as number] || text || '-'}</span>;
|
||||
}
|
||||
return <span>{text || '-'}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统操作日志
|
||||
*/
|
||||
export default defineComponent({
|
||||
name: 'SystemLogs',
|
||||
setup() {
|
||||
const {
|
||||
filterForm,
|
||||
loading,
|
||||
dataSource,
|
||||
columns,
|
||||
pagination,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handlePageChange,
|
||||
} = useLogModel();
|
||||
|
||||
const { containerRef, height } = useContainerSize({ headerOffset: 55 });
|
||||
|
||||
// 首次进入自动加载
|
||||
onMounted(() => {
|
||||
handleSearch();
|
||||
});
|
||||
|
||||
return () => (
|
||||
<div class={pageStyles.containerMain}>
|
||||
{/* ===== 筛选区 ===== */}
|
||||
<div class={pageStyles.filter}>
|
||||
<Form style={{ rowGap: '10px' }} layout="inline" model={filterForm}>
|
||||
<Form.Item label="操作时间" name="actionTimeRange">
|
||||
<RangePicker
|
||||
value={filterForm.actionTimeRange as any}
|
||||
style={{ width: '320px' }}
|
||||
allowClear
|
||||
onUpdate:value={(val: any) => (filterForm.actionTimeRange = val)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="操作类型" name="actionType">
|
||||
<Select
|
||||
value={filterForm.actionType}
|
||||
options={ACTION_TYPE_OPTIONS as any}
|
||||
style={{ width: '140px' }}
|
||||
allowClear
|
||||
placeholder="全部"
|
||||
onUpdate:value={(val: any) => (filterForm.actionType = val || '')}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="操作人" name="searchOperator">
|
||||
<Input
|
||||
v-model:value={filterForm.searchOperator}
|
||||
placeholder="请输入"
|
||||
style={{ width: '160px' }}
|
||||
allowClear
|
||||
onPressEnter={handleSearch}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button onClick={handleReset}>重置</Button>
|
||||
<Button type="primary" onClick={handleSearch} loading={loading.value}>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
{/* ===== 表格区 ===== */}
|
||||
<div class={pageStyles.table}>
|
||||
<div ref={containerRef} class={pageStyles.tableBody}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={dataSource.value}
|
||||
loading={loading.value}
|
||||
scroll={{ x: 'max-content', y: height.value }}
|
||||
pagination={false}
|
||||
>
|
||||
{{
|
||||
bodyCell: (args: any) => renderBodyCell(args),
|
||||
}}
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 独立分页 */}
|
||||
<div class={pageStyles.pagination}>
|
||||
<Pagination
|
||||
current={pagination.value.current}
|
||||
pageSize={pagination.value.pageSize}
|
||||
total={pagination.value.total}
|
||||
showSizeChanger
|
||||
showTotal={(total: number) => `共 ${total} 条`}
|
||||
onChange={handlePageChange}
|
||||
onShowSizeChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { computed, reactive, toRef, Ref } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { useState, useDebounce, useThrottleFn } from '@/hooks';
|
||||
import {
|
||||
getOperationLogList,
|
||||
type SysOperationLogVO,
|
||||
type OperationLogQueryParams,
|
||||
} from '@/api/logs';
|
||||
|
||||
// ============================================================
|
||||
// 常量
|
||||
// ============================================================
|
||||
|
||||
/** 操作类型选项(筛选),value 对应 API type 字段 */
|
||||
export const ACTION_TYPE_OPTIONS = [
|
||||
{ value: '', label: '全部' },
|
||||
{ value: '1', label: '下架赛事' },
|
||||
{ value: '2', label: '上架赛事' },
|
||||
{ value: '3', label: '禁用用户' },
|
||||
{ value: '4', label: '启用用户' },
|
||||
] as const;
|
||||
|
||||
/** 操作类型文案映射 */
|
||||
export const ACTION_TYPE_MAP: Record<number, string> = {
|
||||
1: '下架赛事',
|
||||
2: '上架赛事',
|
||||
3: '禁用用户',
|
||||
4: '启用用户',
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Model
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 系统操作日志页数据模型
|
||||
*/
|
||||
export function useLogModel() {
|
||||
// ===== 筛选条件 =====
|
||||
const filterForm = reactive({
|
||||
actionTimeRange: null as [string, string] | null,
|
||||
actionType: '',
|
||||
searchOperator: '',
|
||||
});
|
||||
|
||||
// 操作人字段防抖 300ms
|
||||
const { debouncedValue: debouncedOperator } = useDebounce(
|
||||
toRef(filterForm, 'searchOperator') as Ref<string>,
|
||||
{ delay: 300 },
|
||||
);
|
||||
|
||||
// ===== 表格状态 =====
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [dataSource, setDataSource] = useState<SysOperationLogVO[]>([]);
|
||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
||||
|
||||
// ===== 表格列配置(字段名对齐 API) =====
|
||||
const columns = [
|
||||
{ title: '操作类型', dataIndex: 'type', key: 'type', width: 140 },
|
||||
{ title: '操作人', dataIndex: 'nickname', key: 'nickname', width: 140 },
|
||||
{ title: '操作内容', dataIndex: 'content', key: 'content', minWidth: 360 },
|
||||
{ title: '操作时间', dataIndex: 'createDate', key: 'createDate', width: 180 },
|
||||
];
|
||||
|
||||
// ===== 计算属性 =====
|
||||
const hasFilter = computed(() => {
|
||||
return (
|
||||
filterForm.actionTimeRange !== null ||
|
||||
filterForm.actionType !== '' ||
|
||||
debouncedOperator.value.trim() !== ''
|
||||
);
|
||||
});
|
||||
|
||||
// ===== 构建 API 查询参数 =====
|
||||
const buildQueryParams = (): OperationLogQueryParams => {
|
||||
const params: OperationLogQueryParams = {
|
||||
page: String(pagination.value.current),
|
||||
limit: String(pagination.value.pageSize),
|
||||
};
|
||||
|
||||
// 时间范围 → dateBegin / dateEnd
|
||||
if (filterForm.actionTimeRange) {
|
||||
const [start, end] = filterForm.actionTimeRange;
|
||||
if (start) params.dateBegin = dayjs(start).format('YYYY-MM-DD');
|
||||
if (end) params.dateEnd = dayjs(end).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
// 操作类型(仅非空时传参)
|
||||
if (filterForm.actionType) {
|
||||
params.type = filterForm.actionType;
|
||||
}
|
||||
|
||||
// 操作人昵称(防抖后的值)
|
||||
if (debouncedOperator.value.trim()) {
|
||||
params.nickname = debouncedOperator.value.trim();
|
||||
}
|
||||
|
||||
return params;
|
||||
};
|
||||
|
||||
// ===== 方法 =====
|
||||
|
||||
/** 查询(节流 500ms) */
|
||||
const handleSearch = useThrottleFn(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const queryParams = buildQueryParams();
|
||||
const res = await getOperationLogList(queryParams);
|
||||
if (res.code === 200) {
|
||||
setDataSource(res.data.list);
|
||||
setPagination({ ...pagination.value, total: res.data.total });
|
||||
}
|
||||
} catch {
|
||||
// 网络层已统一提示
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
/** 重置(重置后自动查询) */
|
||||
const handleReset = useThrottleFn(() => {
|
||||
filterForm.actionTimeRange = null;
|
||||
filterForm.actionType = '';
|
||||
filterForm.searchOperator = '';
|
||||
setPagination({ current: 1, pageSize: 10, total: 0 });
|
||||
setDataSource([]);
|
||||
setTimeout(() => handleSearch(), 350);
|
||||
}, 500);
|
||||
|
||||
const handlePageChange = (page: number, pageSize: number) => {
|
||||
setPagination({ ...pagination.value, current: page, pageSize });
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
return {
|
||||
filterForm,
|
||||
loading,
|
||||
dataSource,
|
||||
columns,
|
||||
pagination,
|
||||
hasFilter,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handlePageChange,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user