feat: 处理 ignore 干扰问题 优化文件依赖
This commit is contained in:
+5
-1
@@ -1,5 +1,5 @@
|
|||||||
# Logs
|
# Logs
|
||||||
logs
|
/logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
@@ -40,3 +40,7 @@ coverage
|
|||||||
.cache
|
.cache
|
||||||
.temp
|
.temp
|
||||||
.tmp
|
.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 {
|
interface MockTournamentAdminOperationPageVO {
|
||||||
OperationLogQueryParams,
|
opName: string;
|
||||||
PageData,
|
source: string;
|
||||||
TournamentAdminOperationPageVO,
|
nickname: string;
|
||||||
} from '@/api/logs/types';
|
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: '创建赛事',
|
opName: '创建赛事',
|
||||||
source: 'PC',
|
source: 'PC',
|
||||||
@@ -33,8 +52,8 @@ const MOCK_EVENT_LOGS: TournamentAdminOperationPageVO[] = [
|
|||||||
export function buildMockEventOperationLogPage(
|
export function buildMockEventOperationLogPage(
|
||||||
page: number,
|
page: number,
|
||||||
limit: number,
|
limit: number,
|
||||||
params: OperationLogQueryParams,
|
params: MockEventOperationLogQueryParams,
|
||||||
): PageData<TournamentAdminOperationPageVO> {
|
): MockPageData<MockTournamentAdminOperationPageVO> {
|
||||||
let filtered = [...MOCK_EVENT_LOGS];
|
let filtered = [...MOCK_EVENT_LOGS];
|
||||||
|
|
||||||
if (params.source) {
|
if (params.source) {
|
||||||
|
|||||||
@@ -2,12 +2,28 @@
|
|||||||
* 操作日志假数据
|
* 操作日志假数据
|
||||||
* 对应接口:GET /admin/sys/operation/page
|
* 对应接口: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,
|
type: 1,
|
||||||
nickname: '张三',
|
nickname: '张三',
|
||||||
@@ -133,7 +149,7 @@ const MOCK_LOGS: SysOperationLogVO[] = [
|
|||||||
/** 按日期范围过滤(dateBegin / dateEnd 格式 yyyy-MM-dd) */
|
/** 按日期范围过滤(dateBegin / dateEnd 格式 yyyy-MM-dd) */
|
||||||
function inDateRange(dateStr: string, begin?: string, end?: string): boolean {
|
function inDateRange(dateStr: string, begin?: string, end?: string): boolean {
|
||||||
if (!begin && !end) return true;
|
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 (begin && d < begin) return false;
|
||||||
if (end && d > end) return false;
|
if (end && d > end) return false;
|
||||||
return true;
|
return true;
|
||||||
@@ -142,21 +158,18 @@ function inDateRange(dateStr: string, begin?: string, end?: string): boolean {
|
|||||||
export function buildMockOperationLogPage(
|
export function buildMockOperationLogPage(
|
||||||
page: number,
|
page: number,
|
||||||
limit: number,
|
limit: number,
|
||||||
params: OperationLogQueryParams,
|
params: MockOperationLogQueryParams,
|
||||||
): PageData<SysOperationLogVO> {
|
): MockPageData<MockSysOperationLogVO> {
|
||||||
let filtered = [...MOCK_LOGS];
|
let filtered = [...MOCK_LOGS];
|
||||||
|
|
||||||
// 按操作类型筛选(API 传 '1'/'2'/'3'/'4')
|
|
||||||
if (params.type) {
|
if (params.type) {
|
||||||
filtered = filtered.filter((item) => item.type === Number(params.type));
|
filtered = filtered.filter((item) => item.type === Number(params.type));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按操作人昵称模糊筛选
|
|
||||||
if (params.nickname) {
|
if (params.nickname) {
|
||||||
filtered = filtered.filter((item) => item.nickname.includes(params.nickname!));
|
filtered = filtered.filter((item) => item.nickname.includes(params.nickname!));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按日期范围筛选
|
|
||||||
filtered = filtered.filter((item) =>
|
filtered = filtered.filter((item) =>
|
||||||
inDateRange(item.createDate, params.dateBegin, params.dateEnd),
|
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