fix: 取消vue原生钩子函数,对接系统管理页面中的接口

This commit is contained in:
ZhuRui
2026-08-05 14:33:21 +08:00
parent 2e6f7ca552
commit 503ee266b0
22 changed files with 569 additions and 447 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ export async function getRoleList(
const limit = Number(params.limit) || 10; const limit = Number(params.limit) || 10;
return { code: 200, msg: 'success', data: buildMockRoleListPage(page, limit) }; return { code: 200, msg: 'success', data: buildMockRoleListPage(page, limit) };
} }
return get('/sys/role/page', params as Record<string, any>); return get('/admin/sys/role/page', params as Record<string, any>);
} }
/** GET /sys/role/usageUserPage — 角色下用户列表分页 */ /** GET /sys/role/usageUserPage — 角色下用户列表分页 */
+4 -4
View File
@@ -41,16 +41,16 @@ export async function getUserList(
return get('/admin/sys/user/page', params as Record<string, any>); return get('/admin/sys/user/page', params as Record<string, any>);
} }
/** POST /sys/user/save — 新增用户 */ /** POST /admin/sys/user/save — 新增用户 */
export function saveUser(params: UserSaveParams): Promise<ApiResult<Record<string, never>>> { export function saveUser(params: UserSaveParams): Promise<ApiResult<Record<string, never>>> {
if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} })); if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} }));
return post('/sys/user/save', params); return post('/admin/sys/user/save', params);
} }
/** POST /sys/user/update — 更新用户 */ /** POST /admin/sys/user/update — 更新用户 */
export function updateUser(params: UserUpdateParams): Promise<ApiResult<Record<string, never>>> { export function updateUser(params: UserUpdateParams): Promise<ApiResult<Record<string, never>>> {
if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} })); if (USE_MOCK) return delay(MOCK_DELAY).then(() => ({ code: 200, msg: 'success', data: {} }));
return post('/sys/user/update', params); return post('/admin/sys/user/update', params);
} }
/** POST /sys/user/active — 启用/禁用(直接调接口) */ /** POST /sys/user/active — 启用/禁用(直接调接口) */
+4 -8
View File
@@ -1,4 +1,4 @@
import { defineComponent, onMounted } from 'vue'; import { defineComponent } from 'vue';
import { import {
Button, Button,
Input, Input,
@@ -171,10 +171,6 @@ export default defineComponent({
}, },
]; ];
onMounted(() => {
handleSearch();
});
return () => ( return () => (
<div class={pageStyles.containerMain}> <div class={pageStyles.containerMain}>
<div class={pageStyles.filter}> <div class={pageStyles.filter}>
@@ -237,9 +233,9 @@ export default defineComponent({
<div class={pageStyles.pagination}> <div class={pageStyles.pagination}>
<Pagination <Pagination
current={pagination.value.current} current={(pagination as any).current.value}
pageSize={pagination.value.pageSize} pageSize={(pagination as any).pageSize.value}
total={pagination.value.total} total={(pagination as any).total.value}
showSizeChanger showSizeChanger
showTotal={(total: number) => `${total}`} showTotal={(total: number) => `${total}`}
onChange={handlePageChange} onChange={handlePageChange}
+32 -40
View File
@@ -2,6 +2,9 @@ import { computed, reactive, toRef, Ref, h } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { StatusTag, type StatusTagTone } from '@/components'; import { StatusTag, type StatusTagTone } from '@/components';
import { useState, useDebounce, useThrottleFn } from '@/hooks'; import { useState, useDebounce, useThrottleFn } from '@/hooks';
import { useRequest } from '@/hooks/useRequest';
import { useEffect } from '@/hooks/useEffect';
import { usePagination } from '@/hooks/usePagination';
import { import {
getBannerList, getBannerList,
delBanner, delBanner,
@@ -61,21 +64,32 @@ export function useBannerModel() {
{ delay: 300 }, { delay: 300 },
); );
// ===== 表格状态(API 返回全量,前端分页) ===== // ===== 数据请求(API 返回全量,前端分页) =====
const [loading, setLoading] = useState<boolean>(false); const {
const [allData, setAllData] = useState<TournamentAdminBannerListVO[]>([]); data,
const [pagination, setPagination] = useState({ loading,
current: 1, run: fetchList,
pageSize: 10, } = useRequest<TournamentAdminBannerListVO[]>(() => getBannerList(buildQueryParams()), {
total: 0, refreshDeps: [],
formatResult: (res) => (res.code == 200 ? res.data : []),
}); });
const allData = computed(() => data.value || []);
// ===== 前端分页 =====
const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
/** 当前页数据(客户端切片) */ /** 当前页数据(客户端切片) */
const dataSource = computed(() => { const dataSource = computed(() => {
const start = (pagination.value.current - 1) * pagination.value.pageSize; const start = ((pagination as any).current.value - 1) * (pagination as any).pageSize.value;
return allData.value.slice(start, start + pagination.value.pageSize); return allData.value.slice(start, start + (pagination as any).pageSize.value);
}); });
// 同步前端分页 total
useEffect(() => {
pagination.setTotal(allData.value.length);
}, [allData]);
// ===== 弹窗状态 ===== // ===== 弹窗状态 =====
const [modalVisible, setModalVisible] = useState<boolean>(false); const [modalVisible, setModalVisible] = useState<boolean>(false);
const [editingRecord, setEditingRecord] = useState<any>(null); const [editingRecord, setEditingRecord] = useState<any>(null);
@@ -119,37 +133,21 @@ export function useBannerModel() {
// ===== 方法 ===== // ===== 方法 =====
/** 查询 */ /** 查询 */
const handleSearch = useThrottleFn(async () => { const handleSearch = () => fetchList();
setLoading(true);
try {
const queryParams = buildQueryParams();
console.log('Banner列表查询参数:', queryParams);
const res = await getBannerList(queryParams);
if (res.code == 200) {
setAllData(res.data);
setPagination({ current: 1, pageSize: 10, total: res.data.length });
} else {
message.error(res.msg || '查询失败');
}
} catch (error: any) {
console.error('Banner查询失败:', error);
} finally {
setLoading(false);
}
}, 500);
/** 重置 */ /** 重置 */
const handleReset = useThrottleFn(() => { const handleReset = useThrottleFn(() => {
filterForm.title = ''; filterForm.title = '';
filterForm.status = ''; filterForm.status = '';
setPagination({ current: 1, pageSize: 10, total: 0 }); pagination.reset();
setAllData([]); setTimeout(fetchList, 350);
setTimeout(() => handleSearch(), 350);
}, 500); }, 500);
const handlePageChange = (page: number, pageSize: number) => { const handlePageChange = (page: number, pageSize: number) => {
setPagination({ ...pagination.value, current: page, pageSize }); pagination.setCurrent(page);
if (pageSize !== (pagination as any).pageSize.value) {
pagination.setPageSize(pageSize);
}
}; };
/** 打开新增弹窗 */ /** 打开新增弹窗 */
@@ -214,39 +212,33 @@ export function useBannerModel() {
/** 删除 */ /** 删除 */
const handleDelete = useThrottleFn(async (record: any) => { const handleDelete = useThrottleFn(async (record: any) => {
setLoading(true);
try { try {
const res = await delBanner(record.id); const res = await delBanner(record.id);
if (res.code == 200) { if (res.code == 200) {
message.success('删除成功'); message.success('删除成功');
handleSearch(); fetchList();
} else { } else {
message.error(res.msg || '删除失败'); message.error(res.msg || '删除失败');
} }
} catch (e: any) { } catch (e: any) {
console.error('Banner删除失败:', e); console.error('Banner删除失败:', e);
} finally {
setLoading(false);
} }
}, 500); }, 500);
/** 启用/禁用切换 */ /** 启用/禁用切换 */
const handleToggleStatus = useThrottleFn(async (record: any) => { const handleToggleStatus = useThrottleFn(async (record: any) => {
setLoading(true);
try { try {
const newStatus = record.status === '1' ? '0' : '1'; const newStatus = record.status === '1' ? '0' : '1';
const actionText = newStatus === '1' ? '启用' : '禁用'; const actionText = newStatus === '1' ? '启用' : '禁用';
const res = await toggleBannerActive({ id: record.id, status: newStatus }); const res = await toggleBannerActive({ id: record.id, status: newStatus });
if (res.code == 200) { if (res.code == 200) {
message.success(`${actionText}成功`); message.success(`${actionText}成功`);
handleSearch(); fetchList();
} else { } else {
message.error(res.msg || '操作失败'); message.error(res.msg || '操作失败');
} }
} catch (e: any) { } catch (e: any) {
console.error('Banner状态切换失败:', e); console.error('Banner状态切换失败:', e);
} finally {
setLoading(false);
} }
}, 500); }, 500);
+10 -8
View File
@@ -1,4 +1,4 @@
import { defineComponent, ref, onMounted, onUnmounted, nextTick } from 'vue'; import { defineComponent, ref, nextTick } from 'vue';
import { import {
Button, Button,
Input, Input,
@@ -12,7 +12,7 @@ import {
} from 'ant-design-vue'; } from 'ant-design-vue';
import { useLogModel, ACTION_TYPE_OPTIONS, ACTION_SOURCE_OPTIONS } from './model/useLogModel'; import { useLogModel, ACTION_TYPE_OPTIONS, ACTION_SOURCE_OPTIONS } from './model/useLogModel';
import { useLogColumns } from './model/useLogColumns'; import { useLogColumns } from './model/useLogColumns';
import { useState, useContainerSize } from '@/hooks'; import { useState, useEffect, useContainerSize } from '@/hooks';
import pageStyles from '@/assets/styles/pageLayout.module.less'; import pageStyles from '@/assets/styles/pageLayout.module.less';
import './index.module.less'; import './index.module.less';
@@ -29,15 +29,17 @@ const LogContentCell = defineComponent({
if (el) setOverflow(el.scrollHeight > el.clientHeight); if (el) setOverflow(el.scrollHeight > el.clientHeight);
}; };
let observer: ResizeObserver | null = null; let observer: ResizeObserver | null = null;
onMounted(() => { useEffect(() => {
nextTick(checkOverflow);
const el = textRef.value; const el = textRef.value;
if (el) { if (!el) return;
nextTick(checkOverflow);
observer = new ResizeObserver(checkOverflow); observer = new ResizeObserver(checkOverflow);
observer.observe(el); observer.observe(el);
} return () => {
}); observer?.disconnect();
onUnmounted(() => observer?.disconnect()); observer = null;
};
}, [() => textRef.value]);
return () => { return () => {
const raw = props.text; const raw = props.text;
return ( return (
+4 -8
View File
@@ -1,4 +1,4 @@
import { defineComponent, onMounted } from 'vue'; import { defineComponent } from 'vue';
import { Button, Input, Table, Form, Space, Pagination, Select, DatePicker } from 'ant-design-vue'; import { Button, Input, Table, Form, Space, Pagination, Select, DatePicker } from 'ant-design-vue';
import { usePaymentsModel } from './model/usePaymentsModel'; import { usePaymentsModel } from './model/usePaymentsModel';
import { useContainerSize } from '@/hooks'; import { useContainerSize } from '@/hooks';
@@ -24,10 +24,6 @@ export default defineComponent({
const { containerRef, height } = useContainerSize({ headerOffset: 55 }); const { containerRef, height } = useContainerSize({ headerOffset: 55 });
onMounted(() => {
handleSearch();
});
return () => ( return () => (
<div class={pageStyles.containerMain}> <div class={pageStyles.containerMain}>
<div class={pageStyles.filter}> <div class={pageStyles.filter}>
@@ -101,9 +97,9 @@ export default defineComponent({
</div> </div>
<div class={pageStyles.pagination}> <div class={pageStyles.pagination}>
<Pagination <Pagination
current={pagination.value.current} current={(pagination as any).current.value}
pageSize={pagination.value.pageSize} pageSize={(pagination as any).pageSize.value}
total={pagination.value.total} total={(pagination as any).total.value}
showSizeChanger showSizeChanger
showTotal={(total: number) => `${total}`} showTotal={(total: number) => `${total}`}
onChange={handlePageChange} onChange={handlePageChange}
@@ -1,5 +1,5 @@
import { computed, reactive, toRef, Ref } from 'vue'; import { computed, reactive, toRef, Ref } from 'vue';
import { useState, useDebounce, useThrottleFn } from '@/hooks'; import { useDebounce, useThrottleFn, useRequest, usePagination, useEffect } from '@/hooks';
import { getPaymentFlowList, type PaymentFlowVO, type PaymentFlowQueryParams } from './services'; import { getPaymentFlowList, type PaymentFlowVO, type PaymentFlowQueryParams } from './services';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -40,13 +40,50 @@ export function usePaymentsModel() {
{ delay: 300 }, { delay: 300 },
); );
// ===== 汇总 ===== // ===== 分页 =====
const [summary, setSummary] = useState({ totalAmount: '0.00', totalRefundAmount: '0.00' }); const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
// ===== 表格状态 ===== // ===== 构建 API 查询参数 =====
const [loading, setLoading] = useState<boolean>(false); const buildQueryParams = (): PaymentFlowQueryParams => {
const [dataSource, setDataSource] = useState<PaymentFlowVO[]>([]); const params: PaymentFlowQueryParams = {
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); page: String((pagination as any).current.value),
limit: String((pagination as any).pageSize.value),
};
if (filterForm.timeRange) {
const [start, end] = filterForm.timeRange;
if (start) params.createDateBegin = dayjs(start).format('YYYY-MM-DD');
if (end) params.createDateEnd = dayjs(end).format('YYYY-MM-DD');
}
if (debouncedNickname.value.trim()) params.nickname = debouncedNickname.value.trim();
if (debouncedPhone.value.trim()) params.phone = debouncedPhone.value.trim();
if (filterForm.type) params.type = filterForm.type;
return params;
};
// ===== 请求 =====
const {
data,
loading,
run: fetchList,
} = useRequest(() => getPaymentFlowList(buildQueryParams()), {
refreshDeps: [],
formatResult: (res) => res,
});
const listData = computed(() => (data.value as any)?.data);
const dataSource = computed(() => listData.value?.list || []);
// 汇总:从 listData(即 res.data,包含 exData)派生
const summary = computed(() => ({
totalAmount: listData.value?.exData?.totalAmount ?? '0.00',
totalRefundAmount: listData.value?.exData?.totalRefundAmount ?? '0.00',
}));
// 同步 total 到 pagination
useEffect(() => {
const total = (data.value as any)?.data?.total;
if (total !== undefined) pagination.setTotal(total);
}, [data]);
// ===== 表格列(dataIndex 对齐 API ===== // ===== 表格列(dataIndex 对齐 API =====
const columns = [ const columns = [
@@ -81,59 +118,25 @@ export function usePaymentsModel() {
); );
}); });
// ===== 构建 API 查询参数 =====
const buildQueryParams = (): PaymentFlowQueryParams => {
const params: PaymentFlowQueryParams = {
page: String(pagination.value.current),
limit: String(pagination.value.pageSize),
};
if (filterForm.timeRange) {
const [start, end] = filterForm.timeRange;
if (start) params.createDateBegin = dayjs(start).format('YYYY-MM-DD');
if (end) params.createDateEnd = dayjs(end).format('YYYY-MM-DD');
}
if (debouncedNickname.value.trim()) params.nickname = debouncedNickname.value.trim();
if (debouncedPhone.value.trim()) params.phone = debouncedPhone.value.trim();
if (filterForm.type) params.type = filterForm.type;
return params;
};
// ===== 方法 ===== // ===== 方法 =====
const handleSearch = useThrottleFn(async () => { const handleSearch = () => fetchList();
setLoading(true);
try {
const queryParams = buildQueryParams();
const res = await getPaymentFlowList(queryParams);
if (res.code == 200) {
setDataSource(res.data.list);
setPagination({ ...pagination.value, total: res.data.total });
setSummary({
totalAmount: res.data.exData?.totalAmount ?? '0.00',
totalRefundAmount: res.data.exData?.totalRefundAmount ?? '0.00',
});
}
} catch {
// 网络层已统一提示
} finally {
setLoading(false);
}
}, 500);
const handleReset = useThrottleFn(() => { const handleReset = useThrottleFn(() => {
filterForm.timeRange = null; filterForm.timeRange = null;
filterForm.nickname = ''; filterForm.nickname = '';
filterForm.phone = ''; filterForm.phone = '';
filterForm.type = ''; filterForm.type = '';
setPagination({ current: 1, pageSize: 10, total: 0 }); pagination.reset();
setDataSource([]); setTimeout(fetchList, 350);
setSummary({ totalAmount: '0.00', totalRefundAmount: '0.00' });
setTimeout(() => handleSearch(), 350);
}, 500); }, 500);
const handlePageChange = (page: number, pageSize: number) => { const handlePageChange = (page: number, pageSize: number) => {
setPagination({ ...pagination.value, current: page, pageSize }); pagination.setCurrent(page);
handleSearch(); if (pageSize !== (pagination as any).pageSize.value) {
pagination.setPageSize(pageSize);
}
setTimeout(fetchList, 0);
}; };
return { return {
+13 -12
View File
@@ -1,7 +1,8 @@
import { defineComponent, ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'; import { defineComponent, ref, nextTick } from 'vue';
import { Select, Table } from 'ant-design-vue'; import { Select, Table } from 'ant-design-vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { useReportsModel } from './model/useReportsModel'; import { useReportsModel } from './model/useReportsModel';
import { useEffect } from '@/hooks';
import styles from './index.module.less'; import styles from './index.module.less';
/** /**
@@ -140,23 +141,23 @@ export default defineComponent({
chartInstance?.resize(); chartInstance?.resize();
}; };
onMounted(() => { // ECharts 挂载/销毁 + 窗口 resize 监听
nextTick(() => { useEffect(() => {
renderChart(); const el = chartRef.value;
}); if (!el) return;
nextTick(() => renderChart());
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
}); return () => {
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
chartInstance?.dispose(); chartInstance?.dispose();
chartInstance = null; chartInstance = null;
}); };
}, [() => chartRef.value]);
// 时间范围切换时刷新图表(接入 API 后生效) // 时间范围切换时刷新图表
watch(range, () => { useEffect(() => {
renderChart(); renderChart();
}); }, [range]);
return () => { return () => {
/** 根据增长率构建箭头 + 颜色(正值 ↑ 绿,负值 ↓ 红,null 时固定文本) */ /** 根据增长率构建箭头 + 颜色(正值 ↑ 绿,负值 ↓ 红,null 时固定文本) */
+4 -8
View File
@@ -1,4 +1,4 @@
import { defineComponent, onMounted } from 'vue'; import { defineComponent } from 'vue';
import { Button, Input, Table, Form, Space, Pagination } from 'ant-design-vue'; import { Button, Input, Table, Form, Space, Pagination } from 'ant-design-vue';
import { WalletOutlined, LockOutlined, RiseOutlined } from '@ant-design/icons-vue'; import { WalletOutlined, LockOutlined, RiseOutlined } from '@ant-design/icons-vue';
import { useWalletModel } from './model/useWalletModel'; import { useWalletModel } from './model/useWalletModel';
@@ -68,10 +68,6 @@ export default defineComponent({
}, },
]; ];
onMounted(() => {
handleSearch();
});
return () => { return () => {
const statCards: StatCardItem[] = [ const statCards: StatCardItem[] = [
{ {
@@ -206,9 +202,9 @@ export default defineComponent({
</div> </div>
<div class={pageStyles.pagination}> <div class={pageStyles.pagination}>
<Pagination <Pagination
current={pagination.value.current} current={(pagination as any).current.value}
pageSize={pagination.value.pageSize} pageSize={(pagination as any).pageSize.value}
total={pagination.value.total} total={(pagination as any).total.value}
showSizeChanger showSizeChanger
showTotal={(total: number) => `${total}`} showTotal={(total: number) => `${total}`}
onChange={handlePageChange} onChange={handlePageChange}
@@ -1,5 +1,12 @@
import { computed, reactive, toRef, Ref } from 'vue'; import { computed, reactive, toRef, Ref } from 'vue';
import { useState, useDebounce, useThrottleFn } from '@/hooks'; import {
useState,
useDebounce,
useThrottleFn,
useRequest,
usePagination,
useEffect,
} from '@/hooks';
import { getWalletList, type WalletSummaryVO, type WalletQueryParams } from './services'; import { getWalletList, type WalletSummaryVO, type WalletQueryParams } from './services';
// ============================================================ // ============================================================
@@ -58,17 +65,58 @@ export function useWalletModel() {
{ delay: 300 }, { delay: 300 },
); );
// ===== 顶部统计(汇总值从接口返回的第一条记录中提取) ===== // ===== 分页 =====
const [summary, setSummary] = useState({ const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
totalBalance: '0.00',
frozenAmount: '0.00', // ===== 构建 API 查询参数 =====
totalWithdraw: '0.00', const buildQueryParams = (): WalletQueryParams => {
// 余额筛选:若起始 > 截止,互换两值
const min = Number(filterForm.minBalance);
const max = Number(filterForm.maxBalance);
if (filterForm.minBalance && filterForm.maxBalance && min > max) {
filterForm.minBalance = filterForm.maxBalance;
filterForm.maxBalance = String(min);
}
const params: WalletQueryParams = {
page: String((pagination as any).current.value),
limit: String((pagination as any).pageSize.value),
};
if (debouncedNickname.value.trim()) params.nickname = debouncedNickname.value.trim();
if (debouncedPhone.value.trim()) params.phone = debouncedPhone.value.trim();
if (filterForm.minBalance) params.amountBegin = filterForm.minBalance;
if (filterForm.maxBalance) params.amountEnd = filterForm.maxBalance;
return params;
};
// ===== 请求 =====
const {
data,
loading,
run: fetchList,
} = useRequest(() => getWalletList(buildQueryParams()), {
refreshDeps: [],
formatResult: (res) => res,
}); });
// ===== 表格状态 ===== const listData = computed(() => (data.value as any)?.data);
const [loading, setLoading] = useState<boolean>(false); const dataSource = computed(() => listData.value?.list || []);
const [dataSource, setDataSource] = useState<WalletSummaryVO[]>([]);
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); // 汇总:从第一条记录提取
const summary = computed(() => {
const first = dataSource.value?.[0] as any;
return {
totalBalance: first?.totalAmount ?? '0.00',
frozenAmount: first?.totalFrozenAmount ?? '0.00',
totalWithdraw: first?.totalWithdrawalAmount ?? '0.00',
};
});
// 同步 total 到 pagination
useEffect(() => {
const total = (data.value as any)?.data?.total;
if (total !== undefined) pagination.setTotal(total);
}, [data]);
// ===== 详情弹窗状态 ===== // ===== 详情弹窗状态 =====
const [detailVisible, setDetailVisible] = useState<boolean>(false); const [detailVisible, setDetailVisible] = useState<boolean>(false);
@@ -112,70 +160,25 @@ export function useWalletModel() {
); );
}); });
// ===== 构建 API 查询参数 =====
const buildQueryParams = (): WalletQueryParams => {
const params: WalletQueryParams = {
page: String(pagination.value.current),
limit: String(pagination.value.pageSize),
};
if (debouncedNickname.value.trim()) params.nickname = debouncedNickname.value.trim();
if (debouncedPhone.value.trim()) params.phone = debouncedPhone.value.trim();
if (filterForm.minBalance) params.amountBegin = filterForm.minBalance;
if (filterForm.maxBalance) params.amountEnd = filterForm.maxBalance;
return params;
};
// ===== 方法 ===== // ===== 方法 =====
const handleSearch = useThrottleFn(async () => { const handleSearch = () => fetchList();
setLoading(true);
try {
// 余额筛选:若起始 > 截止,互换两值
const min = Number(filterForm.minBalance);
const max = Number(filterForm.maxBalance);
if (filterForm.minBalance && filterForm.maxBalance && min > max) {
filterForm.minBalance = filterForm.maxBalance;
filterForm.maxBalance = String(min);
}
const params = buildQueryParams();
console.log('钱包列表查询参数:', params);
const res = await getWalletList(params);
if (res.code == 200) {
console.log('钱包列表查询结果:', { total: res.data.total, count: res.data.list.length });
setDataSource(res.data.list);
setPagination({ ...pagination.value, total: res.data.total });
// 从第一条记录提取汇总值
if (res.data.list.length > 0) {
const first = res.data.list[0];
setSummary({
totalBalance: first.totalAmount || '0.00',
frozenAmount: first.totalFrozenAmount || '0.00',
totalWithdraw: first.totalWithdrawalAmount || '0.00',
});
}
}
} catch {
// 网络层已统一提示
} finally {
setLoading(false);
}
}, 500);
const handleReset = useThrottleFn(() => { const handleReset = useThrottleFn(() => {
filterForm.nickname = ''; filterForm.nickname = '';
filterForm.phone = ''; filterForm.phone = '';
filterForm.minBalance = ''; filterForm.minBalance = '';
filterForm.maxBalance = ''; filterForm.maxBalance = '';
setPagination({ current: 1, pageSize: 10, total: 0 }); pagination.reset();
setDataSource([]); setTimeout(fetchList, 350);
setSummary({ totalBalance: '0.00', frozenAmount: '0.00', totalWithdraw: '0.00' });
setTimeout(() => handleSearch(), 350);
}, 500); }, 500);
const handlePageChange = (page: number, pageSize: number) => { const handlePageChange = (page: number, pageSize: number) => {
setPagination({ ...pagination.value, current: page, pageSize }); pagination.setCurrent(page);
handleSearch(); if (pageSize !== (pagination as any).pageSize.value) {
pagination.setPageSize(pageSize);
}
setTimeout(fetchList, 0);
}; };
const handleViewDetail = (record: any) => { const handleViewDetail = (record: any) => {
+4 -9
View File
@@ -1,4 +1,4 @@
import { defineComponent, onMounted } from 'vue'; import { defineComponent } from 'vue';
import { Button, Input, Table, Form, Space, Pagination, Select } from 'ant-design-vue'; import { Button, Input, Table, Form, Space, Pagination, Select } from 'ant-design-vue';
import { useWithdrawModel, renderAuditStatus, renderPayStatus } from './model/useWithdrawModel'; import { useWithdrawModel, renderAuditStatus, renderPayStatus } from './model/useWithdrawModel';
import { useContainerSize } from '@/hooks'; import { useContainerSize } from '@/hooks';
@@ -82,11 +82,6 @@ export default defineComponent({
}, },
]; ];
// 首次进入自动加载
onMounted(() => {
handleSearch();
});
return () => ( return () => (
<div class={pageStyles.containerMain}> <div class={pageStyles.containerMain}>
{/* ===== 筛选区 ===== */} {/* ===== 筛选区 ===== */}
@@ -175,9 +170,9 @@ export default defineComponent({
{/* 独立分页,右下方 */} {/* 独立分页,右下方 */}
<div class={pageStyles.pagination}> <div class={pageStyles.pagination}>
<Pagination <Pagination
current={pagination.value.current} current={(pagination as any).current.value}
pageSize={pagination.value.pageSize} pageSize={(pagination as any).pageSize.value}
total={pagination.value.total} total={(pagination as any).total.value}
showSizeChanger showSizeChanger
showTotal={(total: number) => `${total}`} showTotal={(total: number) => `${total}`}
onChange={handlePageChange} onChange={handlePageChange}
@@ -1,6 +1,13 @@
import { computed, reactive, toRef, Ref, h } from 'vue'; import { computed, reactive, toRef, Ref, h } from 'vue';
import { StatusTag, type StatusTagTone } from '@/components'; import { StatusTag, type StatusTagTone } from '@/components';
import { useState, useDebounce, useThrottleFn } from '@/hooks'; import {
useState,
useDebounce,
useThrottleFn,
useRequest,
usePagination,
useEffect,
} from '@/hooks';
import { getWithdrawList, type WithdrawVO, type WithdrawQueryParams } from './services'; import { getWithdrawList, type WithdrawVO, type WithdrawQueryParams } from './services';
// ============================================================ // ============================================================
@@ -93,10 +100,41 @@ export function useWithdrawModel() {
{ delay: 300 }, { delay: 300 },
); );
// ===== 表格状态 ===== // ===== 分页 =====
const [loading, setLoading] = useState<boolean>(false); const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
const [dataSource, setDataSource] = useState<WithdrawVO[]>([]);
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); // ===== 构建 API 查询参数 =====
const buildQueryParams = (): WithdrawQueryParams => {
const params: WithdrawQueryParams = {
page: String((pagination as any).current.value),
limit: String((pagination as any).pageSize.value),
};
if (debouncedNickname.value.trim()) params.nickname = debouncedNickname.value.trim();
if (debouncedPhone.value.trim()) params.phone = debouncedPhone.value.trim();
if (debouncedRealName.value.trim()) params.realName = debouncedRealName.value.trim();
if (filterForm.auditStatus) params.auditStatus = filterForm.auditStatus;
if (filterForm.payStatus) params.payStatus = filterForm.payStatus;
return params;
};
// ===== 请求 =====
const {
data,
loading,
run: fetchList,
} = useRequest(() => getWithdrawList(buildQueryParams()), {
refreshDeps: [],
formatResult: (res) => res,
});
const listData = computed(() => (data.value as any)?.data);
const dataSource = computed(() => listData.value?.list || []);
// 同步 total 到 pagination
useEffect(() => {
const total = (data.value as any)?.data?.total;
if (total !== undefined) pagination.setTotal(total);
}, [data]);
// ===== 审核弹窗状态 ===== // ===== 审核弹窗状态 =====
const [auditVisible, setAuditVisible] = useState<boolean>(false); const [auditVisible, setAuditVisible] = useState<boolean>(false);
@@ -139,40 +177,9 @@ export function useWithdrawModel() {
); );
}); });
// ===== 构建 API 查询参数 =====
const buildQueryParams = (): WithdrawQueryParams => {
const params: WithdrawQueryParams = {
page: String(pagination.value.current),
limit: String(pagination.value.pageSize),
};
if (debouncedNickname.value.trim()) params.nickname = debouncedNickname.value.trim();
if (debouncedPhone.value.trim()) params.phone = debouncedPhone.value.trim();
if (debouncedRealName.value.trim()) params.realName = debouncedRealName.value.trim();
if (filterForm.auditStatus) params.auditStatus = filterForm.auditStatus;
if (filterForm.payStatus) params.payStatus = filterForm.payStatus;
return params;
};
// ===== 方法 ===== // ===== 方法 =====
/** 查询(节流 500ms */ const handleSearch = () => fetchList();
const handleSearch = useThrottleFn(async () => {
setLoading(true);
try {
const queryParams = buildQueryParams();
console.log('提现列表查询参数:', queryParams);
const res = await getWithdrawList(queryParams);
if (res.code == 200) {
console.log('提现列表查询结果:', { total: res.data.total, count: res.data.list.length });
setDataSource(res.data.list);
setPagination({ ...pagination.value, total: res.data.total });
}
} catch {
// 网络层已统一提示
} finally {
setLoading(false);
}
}, 500);
/** 重置(重置后自动查询) */ /** 重置(重置后自动查询) */
const handleReset = useThrottleFn(() => { const handleReset = useThrottleFn(() => {
@@ -181,14 +188,16 @@ export function useWithdrawModel() {
filterForm.realName = ''; filterForm.realName = '';
filterForm.auditStatus = ''; filterForm.auditStatus = '';
filterForm.payStatus = ''; filterForm.payStatus = '';
setPagination({ current: 1, pageSize: 10, total: 0 }); pagination.reset();
setDataSource([]); setTimeout(fetchList, 350);
setTimeout(() => handleSearch(), 350);
}, 500); }, 500);
const handlePageChange = (page: number, pageSize: number) => { const handlePageChange = (page: number, pageSize: number) => {
setPagination({ ...pagination.value, current: page, pageSize }); pagination.setCurrent(page);
handleSearch(); if (pageSize !== (pagination as any).pageSize.value) {
pagination.setPageSize(pageSize);
}
setTimeout(fetchList, 0);
}; };
/** 打开审核弹窗 */ /** 打开审核弹窗 */
+4 -9
View File
@@ -1,4 +1,4 @@
import { defineComponent, onMounted } from 'vue'; import { defineComponent } from 'vue';
import { Button, Input, Table, DatePicker, Form, Space, Select, Pagination } from 'ant-design-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 { useLogModel, ACTION_TYPE_OPTIONS, ACTION_TYPE_MAP } from './model/useLogModel';
import { useContainerSize } from '@/hooks'; import { useContainerSize } from '@/hooks';
@@ -35,11 +35,6 @@ export default defineComponent({
const { containerRef, height } = useContainerSize({ headerOffset: 55 }); const { containerRef, height } = useContainerSize({ headerOffset: 55 });
// 首次进入自动加载
onMounted(() => {
handleSearch();
});
return () => ( return () => (
<div class={pageStyles.containerMain}> <div class={pageStyles.containerMain}>
{/* ===== 筛选区 ===== */} {/* ===== 筛选区 ===== */}
@@ -102,9 +97,9 @@ export default defineComponent({
{/* 独立分页 */} {/* 独立分页 */}
<div class={pageStyles.pagination}> <div class={pageStyles.pagination}>
<Pagination <Pagination
current={pagination.value.current} current={(pagination as any).current.value}
pageSize={pagination.value.pageSize} pageSize={(pagination as any).pageSize.value}
total={pagination.value.total} total={(pagination as any).total.value}
showSizeChanger showSizeChanger
showTotal={(total: number) => `${total}`} showTotal={(total: number) => `${total}`}
onChange={handlePageChange} onChange={handlePageChange}
+55 -45
View File
@@ -1,10 +1,15 @@
import { computed, reactive, toRef, Ref } from 'vue'; import { computed, reactive, toRef, Ref } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useState, useDebounce, useThrottleFn } from '@/hooks'; import { useDebounce, useThrottleFn } from '@/hooks';
import { useRequest } from '@/hooks/useRequest';
import { useEffect } from '@/hooks/useEffect';
import { usePagination } from '@/hooks/usePagination';
import { import {
getOperationLogList, getOperationLogList,
type SysOperationLogVO, type SysOperationLogVO,
type OperationLogQueryParams, type OperationLogQueryParams,
type PageData,
type ApiResult,
} from './services'; } from './services';
// ============================================================ // ============================================================
@@ -49,33 +54,14 @@ export function useLogModel() {
{ delay: 300 }, { delay: 300 },
); );
// ===== 表格状态 ===== // ===== 分页 =====
const [loading, setLoading] = useState<boolean>(false); const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
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 查询参数 ===== // ===== 构建 API 查询参数 =====
const buildQueryParams = (): OperationLogQueryParams => { const buildQueryParams = (): OperationLogQueryParams => {
const params: OperationLogQueryParams = { const params: OperationLogQueryParams = {
page: String(pagination.value.current), page: String((pagination as any).current.value),
limit: String(pagination.value.pageSize), limit: String((pagination as any).pageSize.value),
}; };
// 时间范围 → dateBegin / dateEnd // 时间范围 → dateBegin / dateEnd
@@ -98,38 +84,62 @@ export function useLogModel() {
return params; return params;
}; };
// ===== 数据请求 =====
const {
data,
loading,
run: fetchList,
} = useRequest<ApiResult<PageData<SysOperationLogVO>>>(
() => getOperationLogList(buildQueryParams()),
{ refreshDeps: [], formatResult: (res) => res },
);
const dataSource = computed(() => {
const res = data.value;
return res ? (res as any).data?.list || [] : [];
});
useEffect(() => {
const total = (data.value as any)?.data?.total;
if (total !== undefined) pagination.setTotal(total);
}, [data]);
// ===== 表格列配置(字段名对齐 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() !== ''
);
});
// ===== 方法 ===== // ===== 方法 =====
/** 查询(节流 500ms */ const handleSearch = () => fetchList();
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(() => { const handleReset = useThrottleFn(() => {
filterForm.actionTimeRange = null; filterForm.actionTimeRange = null;
filterForm.actionType = ''; filterForm.actionType = '';
filterForm.searchOperator = ''; filterForm.searchOperator = '';
setPagination({ current: 1, pageSize: 10, total: 0 }); pagination.reset();
setDataSource([]); setTimeout(fetchList, 350);
setTimeout(() => handleSearch(), 350);
}, 500); }, 500);
const handlePageChange = (page: number, pageSize: number) => { const handlePageChange = (page: number, pageSize: number) => {
setPagination({ ...pagination.value, current: page, pageSize }); pagination.setCurrent(page);
handleSearch(); if (pageSize !== (pagination as any).pageSize.value) {
pagination.setPageSize(pageSize);
}
setTimeout(fetchList, 0);
}; };
return { return {
@@ -143,7 +143,6 @@ export default defineComponent({
columns={columns} columns={columns}
dataSource={dataSource.value} dataSource={dataSource.value}
loading={loading.value} loading={loading.value}
scroll={{ y: 300 }}
size="small" size="small"
pagination={false} pagination={false}
bordered bordered
@@ -164,7 +163,6 @@ export default defineComponent({
pageSize={pagination.value.pageSize} pageSize={pagination.value.pageSize}
total={pagination.value.total} total={pagination.value.total}
size="small" size="small"
showSizeChanger
showTotal={(total: number) => `${total}`} showTotal={(total: number) => `${total}`}
onChange={handlePageChange} onChange={handlePageChange}
/> />
+4 -9
View File
@@ -1,4 +1,4 @@
import { defineComponent, onMounted } from 'vue'; import { defineComponent } from 'vue';
import { Button, Input, Table, Form, Space, Select, Modal, Pagination } from 'ant-design-vue'; import { Button, Input, Table, Form, Space, Select, Modal, Pagination } from 'ant-design-vue';
import type { ModalProps } from 'ant-design-vue'; import type { ModalProps } from 'ant-design-vue';
import { useRoleModel } from './model/useRoleModel'; import { useRoleModel } from './model/useRoleModel';
@@ -114,11 +114,6 @@ export default defineComponent({
}, },
]; ];
// ===== 初始化 =====
onMounted(() => {
handleSearch();
});
return () => ( return () => (
<div class={pageStyles.containerMain}> <div class={pageStyles.containerMain}>
{/* ===== 筛选区 ===== */} {/* ===== 筛选区 ===== */}
@@ -173,9 +168,9 @@ export default defineComponent({
{/* 分页 */} {/* 分页 */}
<div class={pageStyles.pagination}> <div class={pageStyles.pagination}>
<Pagination <Pagination
current={pagination.value.current} current={(pagination as any).current.value}
pageSize={pagination.value.pageSize} pageSize={(pagination as any).pageSize.value}
total={pagination.value.total} total={(pagination as any).total.value}
showSizeChanger showSizeChanger
showTotal={(total: number) => `${total}`} showTotal={(total: number) => `${total}`}
onChange={handlePageChange} onChange={handlePageChange}
+47 -43
View File
@@ -1,6 +1,9 @@
import { computed, reactive, toRef, Ref } from 'vue'; import { computed, reactive, toRef, Ref } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { useState, useDebounce, useThrottleFn } from '@/hooks'; import { useState, useDebounce, useThrottleFn } from '@/hooks';
import { useRequest } from '@/hooks/useRequest';
import { useEffect } from '@/hooks/useEffect';
import { usePagination } from '@/hooks/usePagination';
import { import {
getRoleList, getRoleList,
saveRole, saveRole,
@@ -8,6 +11,8 @@ import {
deleteRole, deleteRole,
type TournamentAdminRolePageVO, type TournamentAdminRolePageVO,
type RoleListQueryParams, type RoleListQueryParams,
type PageData,
type ApiResult,
} from './services'; } from './services';
// ============================================================ // ============================================================
@@ -27,10 +32,38 @@ export function useRoleModel() {
delay: 300, delay: 300,
}); });
// ===== 表格状态 ===== // ===== 分页 =====
const [loading, setLoading] = useState<boolean>(false); const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
const [dataSource, setDataSource] = useState<TournamentAdminRolePageVO[]>([]);
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); // ===== 构建 API 查询参数 =====
const buildQueryParams = (): RoleListQueryParams => {
const params: RoleListQueryParams = {
page: String((pagination as any).current.value),
limit: String((pagination as any).pageSize.value),
};
if (filterForm.name.trim()) params.name = filterForm.name.trim();
return params;
};
// ===== 数据请求 =====
const {
data,
loading,
run: fetchList,
} = useRequest<ApiResult<PageData<TournamentAdminRolePageVO>>>(
() => getRoleList(buildQueryParams()),
{ refreshDeps: [], formatResult: (res) => res },
);
const dataSource = computed(() => {
const res = data.value;
return res ? (res as any).data?.list || [] : [];
});
useEffect(() => {
const total = (data.value as any)?.data?.total;
if (total !== undefined) pagination.setTotal(total);
}, [data]);
// ===== 弹窗状态 ===== // ===== 弹窗状态 =====
const [formVisible, setFormVisible] = useState<boolean>(false); const [formVisible, setFormVisible] = useState<boolean>(false);
@@ -55,53 +88,27 @@ export function useRoleModel() {
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 200 }, { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 200 },
]; ];
// ===== 构建 API 查询参数 =====
const buildQueryParams = (): RoleListQueryParams => {
const params: RoleListQueryParams = {
page: String(pagination.value.current),
limit: String(pagination.value.pageSize),
};
if (filterForm.name.trim()) params.name = filterForm.name.trim();
return params;
};
// ===== 计算属性 ===== // ===== 计算属性 =====
const hasFilter = computed(() => debouncedName.value.trim() !== ''); const hasFilter = computed(() => debouncedName.value.trim() !== '');
const isEdit = computed(() => editingRecord.value !== null); const isEdit = computed(() => editingRecord.value !== null);
// ===== 方法 ===== // ===== 方法 =====
/** 查询(节流 500ms */ const handleSearch = () => fetchList();
const handleSearch = useThrottleFn(async () => {
setLoading(true);
try {
const queryParams = buildQueryParams();
console.log('角色列表查询参数:', queryParams);
const res = await getRoleList(queryParams);
if (res.code == 200) {
setDataSource(res.data.list);
setPagination({ ...pagination.value, total: res.data.total });
} else {
message.error(res.msg || '查询失败');
}
} catch (error: any) {
console.error('角色列表查询失败:', error);
} finally {
setLoading(false);
}
}, 500);
/** 重置(重置后自动查询) */ /** 重置(重置后自动查询) */
const handleReset = useThrottleFn(() => { const handleReset = useThrottleFn(() => {
filterForm.name = ''; filterForm.name = '';
setPagination({ current: 1, pageSize: 10, total: 0 }); pagination.reset();
setDataSource([]); setTimeout(fetchList, 350);
setTimeout(() => handleSearch(), 350);
}, 500); }, 500);
const handlePageChange = (page: number, pageSize: number) => { const handlePageChange = (page: number, pageSize: number) => {
setPagination({ ...pagination.value, current: page, pageSize }); pagination.setCurrent(page);
handleSearch(); if (pageSize !== (pagination as any).pageSize.value) {
pagination.setPageSize(pageSize);
}
setTimeout(fetchList, 0);
}; };
/** 新增 */ /** 新增 */
@@ -146,7 +153,7 @@ export function useRoleModel() {
if (res.code == 200) { if (res.code == 200) {
message.success(isEdit.value ? '编辑成功' : '新增成功'); message.success(isEdit.value ? '编辑成功' : '新增成功');
handleCloseForm(); handleCloseForm();
handleSearch(); fetchList();
} else { } else {
message.error(res.msg || '操作失败'); message.error(res.msg || '操作失败');
} }
@@ -159,19 +166,16 @@ export function useRoleModel() {
/** 删除 */ /** 删除 */
const handleDelete = useThrottleFn(async (record: any) => { const handleDelete = useThrottleFn(async (record: any) => {
setLoading(true);
try { try {
const res = await deleteRole(record.roleId); const res = await deleteRole(record.roleId);
if (res.code == 200) { if (res.code == 200) {
setDataSource(dataSource.value.filter((item: any) => item.roleId !== record.roleId));
message.success('删除成功'); message.success('删除成功');
fetchList();
} else { } else {
message.error(res.msg || '删除失败'); message.error(res.msg || '删除失败');
} }
} catch { } catch {
// 网络层已统一提示,不再重复 message.error // 网络层已统一提示,不再重复 message.error
} finally {
setLoading(false);
} }
}, 500); }, 500);
@@ -1,8 +1,8 @@
import { defineComponent, ref, reactive } from 'vue'; import { defineComponent, ref, reactive } from 'vue';
import { useEffect, useState } from '@/hooks'; import { useEffect, useState } from '@/hooks';
import { Modal, Form, Input, Select, Button } from 'ant-design-vue'; import { Modal, Form, Input, Select, Button } from 'ant-design-vue';
import { getRoleList, type TournamentAdminRolePageVO } from '../../roles/model/services';
import { import {
ROLE_FORM_OPTIONS,
PASSWORD_PLACEHOLDER, PASSWORD_PLACEHOLDER,
realNameRules, realNameRules,
phoneRules, phoneRules,
@@ -55,12 +55,84 @@ export default defineComponent({
/** 编辑模式下是否处于"重置密码"状态 */ /** 编辑模式下是否处于"重置密码"状态 */
const [isResetting, setIsResetting] = useState(false); const [isResetting, setIsResetting] = useState(false);
// ---- 角色搜索状态 ----
const roleKeyword = ref('');
const [roleOptions, setRoleOptions] = useState<{ value: string; label: string }[]>([]);
const [roleSearchLoading, setRoleSearchLoading] = useState(false);
/** 手动防抖计时器 */
let roleSearchTimer: ReturnType<typeof setTimeout> | null = null;
/** 已加载的全量角色缓存 */
const [allRoleOptions, setAllRoleOptions] = useState<{ value: string; label: string }[]>([]);
// ---- 角色搜索 API 调用(用户主动搜索,带 loading) ----
const doRoleSearch = async (keyword: string) => {
setRoleSearchLoading(true);
try {
const params: Record<string, string> = { page: '1', limit: '999', name: keyword.trim() };
const res = await getRoleList(params);
if (res.code == 200) {
const list: TournamentAdminRolePageVO[] = res.data?.list || [];
setRoleOptions(
list.map((item) => ({
value: String(item.roleId),
label: item.roleName,
})),
);
}
} catch (e) {
console.error('[UserForm] 搜索角色失败:', e);
} finally {
setRoleSearchLoading(false);
}
};
/** 初始加载全量角色(弹窗打开时调用,不带 loading,不覆盖已选值) */
const fetchInitialRoles = async () => {
try {
const res = await getRoleList({ page: '1', limit: '999' });
if (res.code == 200) {
const list: TournamentAdminRolePageVO[] = res.data?.list || [];
const opts = list.map((item) => ({
value: String(item.roleId),
label: item.roleName,
}));
// 确保已选角色在列表中
const selectedId = formData.roleId;
if (selectedId && !opts.some((o) => o.value === selectedId)) {
const cached = allRoleOptions.value.find((o) => o.value === selectedId);
if (cached) opts.unshift(cached);
}
setAllRoleOptions(opts);
setRoleOptions(opts);
}
} catch (e) {
console.error('[UserForm] 加载角色列表失败:', e);
}
};
/** 角色搜索输入(带 350ms 手动防抖) */
const onRoleSearch = (value: string) => {
roleKeyword.value = value;
if (roleSearchTimer) clearTimeout(roleSearchTimer);
if (!value.trim()) {
// 清空搜索:恢复全量角色
setRoleOptions(allRoleOptions.value);
return;
}
roleSearchTimer = setTimeout(() => {
doRoleSearch(value);
}, 350);
};
/** 根据 record 初始化表单 */ /** 根据 record 初始化表单 */
const initFormFromRecord = (record: any) => { const initFormFromRecord = (record: any) => {
if (!record) { if (!record) {
// 新增:预填默认密码 + 确认密码 // 新增:预填默认密码 + 确认密码
Object.assign(formData, getAddForm()); Object.assign(formData, getAddForm());
setIsResetting(false); setIsResetting(false);
roleKeyword.value = '';
fetchInitialRoles();
return; return;
} }
const fresh = getDefaultForm(); const fresh = getDefaultForm();
@@ -71,6 +143,22 @@ export default defineComponent({
roleId: record.roleId || undefined, roleId: record.roleId || undefined,
}); });
setIsResetting(false); setIsResetting(false);
// 编辑模式:先回显已选角色,再静默加载全量角色
roleKeyword.value = '';
if (record.roleId) {
const selectedOpt = {
value: String(record.roleId),
label: record.roleName || String(record.roleId),
};
setAllRoleOptions([selectedOpt]);
setRoleOptions([selectedOpt]);
} else {
setAllRoleOptions([]);
setRoleOptions([]);
}
// 后台静默加载全量角色列表(API 返回后合并到选项,loading 为 false 不影响显示)
fetchInitialRoles();
}; };
/** 监听 visible 变化重置表单 */ /** 监听 visible 变化重置表单 */
@@ -82,6 +170,14 @@ export default defineComponent({
} }
}, [() => props.visible]); }, [() => props.visible]);
/** 关闭时清理防抖计时器 */
useEffect(() => {
if (!props.visible && roleSearchTimer) {
clearTimeout(roleSearchTimer);
roleSearchTimer = null;
}
}, [() => props.visible]);
/** 点击"重置密码" */ /** 点击"重置密码" */
const handleResetPassword = () => { const handleResetPassword = () => {
setIsResetting(true); setIsResetting(true);
@@ -172,9 +268,19 @@ export default defineComponent({
</Form.Item> </Form.Item>
<Form.Item label="角色" name="roleId" rules={roleRules} class={styles.col}> <Form.Item label="角色" name="roleId" rules={roleRules} class={styles.col}>
<Select <Select
options={ROLE_FORM_OPTIONS as any}
placeholder="请选择角色"
v-model:value={formData.roleId} v-model:value={formData.roleId}
showSearch
filterOption={false}
allowClear={!roleSearchLoading.value}
loading={roleSearchLoading.value}
placeholder="请输入关键字搜索角色"
options={roleOptions.value as any}
onSearch={onRoleSearch}
onFocus={() => {
if (formData.roleId && roleOptions.value.length === 0) {
doRoleSearch(formData.roleId);
}
}}
/> />
</Form.Item> </Form.Item>
</div> </div>
@@ -210,9 +316,19 @@ export default defineComponent({
{/* 角色:单独一行 */} {/* 角色:单独一行 */}
<Form.Item label="角色" name="roleId" rules={roleRules}> <Form.Item label="角色" name="roleId" rules={roleRules}>
<Select <Select
options={ROLE_FORM_OPTIONS as any}
placeholder="请选择角色"
v-model:value={formData.roleId} v-model:value={formData.roleId}
showSearch
filterOption={false}
allowClear={!roleSearchLoading.value}
loading={roleSearchLoading.value}
placeholder="请输入关键字搜索角色"
options={roleOptions.value as any}
onSearch={onRoleSearch}
onFocus={() => {
if (formData.roleId && roleOptions.value.length === 0) {
doRoleSearch(formData.roleId);
}
}}
style={{ width: '200px' }} style={{ width: '200px' }}
/> />
</Form.Item> </Form.Item>
-8
View File
@@ -14,14 +14,6 @@ import { isChinaPhone, isValidPassword, isValidRealName } from '@/utils/form';
/** 密码占位符(写死长度,避免暴露真实密码长度) */ /** 密码占位符(写死长度,避免暴露真实密码长度) */
export const PASSWORD_PLACEHOLDER = '********'; export const PASSWORD_PLACEHOLDER = '********';
/** 角色下拉选项(表单使用,value 对应 API roleId */
export const ROLE_FORM_OPTIONS = [
{ value: '1', label: '超级管理员' },
{ value: '2', label: '赛事管理员' },
{ value: '3', label: '裁判' },
{ value: '4', label: '财务' },
] as const;
// ============================================================ // ============================================================
// 真实姓名规则 // 真实姓名规则
// ============================================================ // ============================================================
+7 -11
View File
@@ -1,4 +1,4 @@
import { defineComponent, onMounted } from 'vue'; import { defineComponent } from 'vue';
import { import {
Button, Button,
Input, Input,
@@ -10,7 +10,7 @@ import {
Pagination, Pagination,
message, message,
} from 'ant-design-vue'; } from 'ant-design-vue';
import { useUserModel, ROLE_OPTIONS, USER_STATUS_OPTIONS } from './model/useUserModel'; import { useUserModel, USER_STATUS_OPTIONS } from './model/useUserModel';
import { toggleUserActive, saveUser, updateUser } from './model/services'; import { toggleUserActive, saveUser, updateUser } from './model/services';
import { useState, useThrottleFn, useContainerSize } from '@/hooks'; import { useState, useThrottleFn, useContainerSize } from '@/hooks';
import UserFormModal from './components/UserFormModal'; import UserFormModal from './components/UserFormModal';
@@ -60,6 +60,7 @@ export default defineComponent({
dataSource, dataSource,
columns, columns,
pagination, pagination,
roleOptions,
handleSearch, handleSearch,
handleReset, handleReset,
handlePageChange, handlePageChange,
@@ -175,11 +176,6 @@ export default defineComponent({
}, },
]; ];
// ===== 初始化 =====
onMounted(() => {
handleSearch();
});
return () => ( return () => (
<div class={pageStyles.containerMain}> <div class={pageStyles.containerMain}>
{/* ===== 筛选区 ===== */} {/* ===== 筛选区 ===== */}
@@ -198,7 +194,7 @@ export default defineComponent({
<Form.Item label="角色" name="roleId"> <Form.Item label="角色" name="roleId">
<Select <Select
value={filterForm.roleId} value={filterForm.roleId}
options={ROLE_OPTIONS as any} options={roleOptions.value as any}
style={{ width: '140px' }} style={{ width: '140px' }}
allowClear allowClear
placeholder="全部" placeholder="全部"
@@ -252,9 +248,9 @@ export default defineComponent({
{/* 独立分页 */} {/* 独立分页 */}
<div class={pageStyles.pagination}> <div class={pageStyles.pagination}>
<Pagination <Pagination
current={pagination.value.current} current={(pagination as any).current.value}
pageSize={pagination.value.pageSize} pageSize={(pagination as any).pageSize.value}
total={pagination.value.total} total={(pagination as any).total.value}
showSizeChanger showSizeChanger
showTotal={(total: number) => `${total}`} showTotal={(total: number) => `${total}`}
onChange={handlePageChange} onChange={handlePageChange}
+3 -3
View File
@@ -29,9 +29,9 @@ export type {
// URL 常量 // URL 常量
// ============================================================ // ============================================================
const userPage = '/admin/sys/user/page'; const userPage = '/admin/sys/user/page';
const userSave = '/sys/user/save'; const userSave = '/admin/sys/user/save';
const userUpdate = '/sys/user/update'; const userUpdate = '/admin/sys/user/update';
const userActive = '/sys/user/active'; const userActive = '/admin/sys/user/active';
const userUpdatePwd = '/sys/user/updatepwd'; const userUpdatePwd = '/sys/user/updatepwd';
// ============================================================ // ============================================================
+78 -55
View File
@@ -1,22 +1,23 @@
import { computed, reactive, toRef, Ref, h } from 'vue'; import { computed, reactive, toRef, Ref, h, ref } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { StatusTag, type StatusTagTone } from '@/components'; import { StatusTag, type StatusTagTone } from '@/components';
import { useState, useDebounce, useThrottleFn } from '@/hooks'; import { useDebounce, useThrottleFn } from '@/hooks';
import { getUserList, type TournamentAdminUserVO, type UserListQueryParams } from './services'; import { useRequest } from '@/hooks/useRequest';
import { useEffect } from '@/hooks/useEffect';
import { usePagination } from '@/hooks/usePagination';
import {
getUserList,
type TournamentAdminUserVO,
type UserListQueryParams,
type PageData,
type ApiResult,
} from './services';
import { getRoleList, type TournamentAdminRolePageVO } from '../../roles/model/services';
// ============================================================ // ============================================================
// 常量 // 常量
// ============================================================ // ============================================================
/** 角色选项(筛选) */
export const ROLE_OPTIONS = [
{ value: '', label: '全部' },
{ value: '1', label: '超级管理员' },
{ value: '2', label: '赛事管理员' },
{ value: '3', label: '裁判' },
{ value: '4', label: '财务' },
] as const;
/** 状态选项(筛选) */ /** 状态选项(筛选) */
export const USER_STATUS_OPTIONS = [ export const USER_STATUS_OPTIONS = [
{ value: '', label: '全部' }, { value: '', label: '全部' },
@@ -45,20 +46,66 @@ export function useUserModel() {
status: '' as '' | '0' | '1', status: '' as '' | '0' | '1',
}); });
// ===== 动态角色选项 =====
const roleOptions = ref<{ value: string; label: string }[]>([{ value: '', label: '全部' }]);
/** 加载全部角色列表(用于筛选下拉) */
const fetchRoleOptions = async () => {
try {
const res = await getRoleList({ page: '1', limit: '999' });
if (res.code == 200) {
const list: TournamentAdminRolePageVO[] = res.data?.list || [];
const opts = list.map((item) => ({
value: String(item.roleId),
label: item.roleName,
}));
roleOptions.value = [{ value: '', label: '全部' }, ...opts];
}
} catch (e) {
console.error('[UserModel] 加载角色列表失败:', e);
}
};
// 文本筛选防抖 300ms // 文本筛选防抖 300ms
const { debouncedValue: debouncedText } = useDebounce(toRef(filterForm, 'text') as Ref<string>, { const { debouncedValue: debouncedText } = useDebounce(toRef(filterForm, 'text') as Ref<string>, {
delay: 300, delay: 300,
}); });
// ===== 表格状态 ===== // ===== 分页 =====
const [loading, setLoading] = useState<boolean>(false); const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 });
const [dataSource, setDataSource] = useState<TournamentAdminUserVO[]>([]);
const [pagination, setPagination] = useState({ // ===== 构建 API 查询参数 =====
current: 1, const buildQueryParams = (): UserListQueryParams => {
pageSize: 10, const params: UserListQueryParams = {
total: 0, page: String((pagination as any).current.value),
limit: String((pagination as any).pageSize.value),
};
if (filterForm.text.trim()) params.text = filterForm.text.trim();
if (filterForm.roleId) params.roleId = filterForm.roleId;
if (filterForm.status) params.status = filterForm.status;
return params;
};
// ===== 数据请求 =====
const {
data,
loading,
run: fetchList,
} = useRequest<ApiResult<PageData<TournamentAdminUserVO>>>(
() => getUserList(buildQueryParams()),
{ refreshDeps: [], formatResult: (res) => res },
);
const dataSource = computed(() => {
const res = data.value;
return res ? (res as any).data?.list || [] : [];
}); });
useEffect(() => {
const total = (data.value as any)?.data?.total;
if (total !== undefined) pagination.setTotal(total);
}, [data]);
// ===== 表格列配置 ===== // ===== 表格列配置 =====
const columns = [ const columns = [
{ title: '姓名', dataIndex: 'realName', key: 'realName', width: 120 }, { title: '姓名', dataIndex: 'realName', key: 'realName', width: 120 },
@@ -78,18 +125,6 @@ export function useUserModel() {
{ title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 }, { title: '创建时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
]; ];
// ===== 构建 API 查询参数 =====
const buildQueryParams = (): UserListQueryParams => {
const params: UserListQueryParams = {
page: String(pagination.value.current),
limit: String(pagination.value.pageSize),
};
if (filterForm.text.trim()) params.text = filterForm.text.trim();
if (filterForm.roleId) params.roleId = filterForm.roleId;
if (filterForm.status) params.status = filterForm.status;
return params;
};
// ===== 计算属性 ===== // ===== 计算属性 =====
const hasFilter = computed(() => { const hasFilter = computed(() => {
return ( return (
@@ -99,49 +134,37 @@ export function useUserModel() {
// ===== 方法 ===== // ===== 方法 =====
/** 查询(节流 500ms */ const handleSearch = () => fetchList();
const handleSearch = useThrottleFn(async () => {
setLoading(true);
try {
const queryParams = buildQueryParams();
console.log('用户列表查询参数:', queryParams);
const res = await getUserList(queryParams);
if (res.code == 200) {
setDataSource(res.data.list);
setPagination({ ...pagination.value, total: res.data.total });
} else {
message.error(res.msg || '查询失败');
}
} catch (error: any) {
console.error('用户列表查询失败:', error);
} finally {
setLoading(false);
}
}, 500);
/** 重置(节流 500ms,重置后自动查询) */ /** 重置(节流 500ms,重置后自动查询) */
const handleReset = useThrottleFn(() => { const handleReset = useThrottleFn(() => {
filterForm.text = ''; filterForm.text = '';
filterForm.roleId = ''; filterForm.roleId = '';
filterForm.status = ''; filterForm.status = '';
setPagination({ current: 1, pageSize: 10, total: 0 }); pagination.reset();
setDataSource([]);
// 等待 debounce(300ms) 生效后自动查询 // 等待 debounce(300ms) 生效后自动查询
setTimeout(() => handleSearch(), 350); setTimeout(fetchList, 350);
}, 500); }, 500);
/** 分页变更 */ /** 分页变更 */
const handlePageChange = (page: number, pageSize: number) => { const handlePageChange = (page: number, pageSize: number) => {
setPagination({ ...pagination.value, current: page, pageSize }); pagination.setCurrent(page);
handleSearch(); if (pageSize !== (pagination as any).pageSize.value) {
pagination.setPageSize(pageSize);
}
setTimeout(fetchList, 0);
}; };
// ===== 初始化:加载角色选项 =====
fetchRoleOptions();
return { return {
filterForm, filterForm,
loading, loading,
dataSource, dataSource,
columns, columns,
pagination, pagination,
roleOptions,
hasFilter, hasFilter,
handleSearch, handleSearch,
handleReset, handleReset,