feat: 处理 ignore 干扰问题 优化文件依赖

This commit is contained in:
ZhuRui
2026-07-30 17:23:23 +08:00
parent 6b7a0fd284
commit 7606bb7d8d
10 changed files with 845 additions and 19 deletions
+36
View File
@@ -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;
}
}
}
+210
View File
@@ -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>
);
},
});
+154
View File
@@ -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=PC2=小程序) */
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,
};
}