From 503ee266b0d72d1efcd2d5d52e988fde3cbcd4e3 Mon Sep 17 00:00:00 2001 From: ZhuRui Date: Wed, 5 Aug 2026 14:33:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8F=96=E6=B6=88vue=E5=8E=9F=E7=94=9F?= =?UTF-8?q?=E9=92=A9=E5=AD=90=E5=87=BD=E6=95=B0=EF=BC=8C=E5=AF=B9=E6=8E=A5?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/roles/index.ts | 2 +- src/api/users/index.ts | 8 +- src/pages/events/banner/index.tsx | 12 +- .../events/banner/model/useBannerModel.ts | 72 +++++----- src/pages/events/logs/index.tsx | 22 +-- src/pages/finance/payments/index.tsx | 12 +- .../payments/model/usePaymentsModel.ts | 101 ++++++------- src/pages/finance/reports/index.tsx | 31 ++-- src/pages/finance/wallet/index.tsx | 12 +- .../finance/wallet/model/useWalletModel.ts | 129 ++++++++--------- src/pages/finance/withdraw/index.tsx | 13 +- .../withdraw/model/useWithdrawModel.ts | 93 ++++++------ src/pages/system/logs/index.tsx | 13 +- src/pages/system/logs/model/useLogModel.ts | 100 +++++++------ .../system/roles/components/UserListModal.tsx | 2 - src/pages/system/roles/index.tsx | 13 +- src/pages/system/roles/model/useRoleModel.ts | 90 ++++++------ .../system/users/components/UserFormModal.tsx | 126 ++++++++++++++++- src/pages/system/users/controller.ts | 8 -- src/pages/system/users/index.tsx | 18 +-- src/pages/system/users/model/services.ts | 6 +- src/pages/system/users/model/useUserModel.ts | 133 ++++++++++-------- 22 files changed, 569 insertions(+), 447 deletions(-) diff --git a/src/api/roles/index.ts b/src/api/roles/index.ts index fcda64f..4a0628d 100644 --- a/src/api/roles/index.ts +++ b/src/api/roles/index.ts @@ -38,7 +38,7 @@ export async function getRoleList( const limit = Number(params.limit) || 10; return { code: 200, msg: 'success', data: buildMockRoleListPage(page, limit) }; } - return get('/sys/role/page', params as Record); + return get('/admin/sys/role/page', params as Record); } /** GET /sys/role/usageUserPage — 角色下用户列表分页 */ diff --git a/src/api/users/index.ts b/src/api/users/index.ts index 1ab6212..0375cfc 100644 --- a/src/api/users/index.ts +++ b/src/api/users/index.ts @@ -41,16 +41,16 @@ export async function getUserList( return get('/admin/sys/user/page', params as Record); } -/** POST /sys/user/save — 新增用户 */ +/** POST /admin/sys/user/save — 新增用户 */ export function saveUser(params: UserSaveParams): Promise>> { 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>> { 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 — 启用/禁用(直接调接口) */ diff --git a/src/pages/events/banner/index.tsx b/src/pages/events/banner/index.tsx index af46bac..b7c3722 100644 --- a/src/pages/events/banner/index.tsx +++ b/src/pages/events/banner/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent, onMounted } from 'vue'; +import { defineComponent } from 'vue'; import { Button, Input, @@ -171,10 +171,6 @@ export default defineComponent({ }, ]; - onMounted(() => { - handleSearch(); - }); - return () => (
@@ -237,9 +233,9 @@ export default defineComponent({
`共 ${total} 条`} onChange={handlePageChange} diff --git a/src/pages/events/banner/model/useBannerModel.ts b/src/pages/events/banner/model/useBannerModel.ts index 229d2c5..132c633 100644 --- a/src/pages/events/banner/model/useBannerModel.ts +++ b/src/pages/events/banner/model/useBannerModel.ts @@ -2,6 +2,9 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; import { message } from 'ant-design-vue'; import { StatusTag, type StatusTagTone } from '@/components'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; +import { useRequest } from '@/hooks/useRequest'; +import { useEffect } from '@/hooks/useEffect'; +import { usePagination } from '@/hooks/usePagination'; import { getBannerList, delBanner, @@ -61,21 +64,32 @@ export function useBannerModel() { { delay: 300 }, ); - // ===== 表格状态(API 返回全量,前端分页) ===== - const [loading, setLoading] = useState(false); - const [allData, setAllData] = useState([]); - const [pagination, setPagination] = useState({ - current: 1, - pageSize: 10, - total: 0, + // ===== 数据请求(API 返回全量,前端分页) ===== + const { + data, + loading, + run: fetchList, + } = useRequest(() => getBannerList(buildQueryParams()), { + refreshDeps: [], + formatResult: (res) => (res.code == 200 ? res.data : []), }); + const allData = computed(() => data.value || []); + + // ===== 前端分页 ===== + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); + /** 当前页数据(客户端切片) */ const dataSource = computed(() => { - const start = (pagination.value.current - 1) * pagination.value.pageSize; - return allData.value.slice(start, start + pagination.value.pageSize); + const start = ((pagination as any).current.value - 1) * (pagination as any).pageSize.value; + return allData.value.slice(start, start + (pagination as any).pageSize.value); }); + // 同步前端分页 total + useEffect(() => { + pagination.setTotal(allData.value.length); + }, [allData]); + // ===== 弹窗状态 ===== const [modalVisible, setModalVisible] = useState(false); const [editingRecord, setEditingRecord] = useState(null); @@ -119,37 +133,21 @@ export function useBannerModel() { // ===== 方法 ===== /** 查询 */ - const handleSearch = useThrottleFn(async () => { - 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 handleSearch = () => fetchList(); /** 重置 */ const handleReset = useThrottleFn(() => { filterForm.title = ''; filterForm.status = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setAllData([]); - setTimeout(() => handleSearch(), 350); + pagination.reset(); + setTimeout(fetchList, 350); }, 500); 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) => { - setLoading(true); try { const res = await delBanner(record.id); if (res.code == 200) { message.success('删除成功'); - handleSearch(); + fetchList(); } else { message.error(res.msg || '删除失败'); } } catch (e: any) { console.error('Banner删除失败:', e); - } finally { - setLoading(false); } }, 500); /** 启用/禁用切换 */ const handleToggleStatus = useThrottleFn(async (record: any) => { - setLoading(true); try { const newStatus = record.status === '1' ? '0' : '1'; const actionText = newStatus === '1' ? '启用' : '禁用'; const res = await toggleBannerActive({ id: record.id, status: newStatus }); if (res.code == 200) { message.success(`${actionText}成功`); - handleSearch(); + fetchList(); } else { message.error(res.msg || '操作失败'); } } catch (e: any) { console.error('Banner状态切换失败:', e); - } finally { - setLoading(false); } }, 500); diff --git a/src/pages/events/logs/index.tsx b/src/pages/events/logs/index.tsx index ea7d58d..431f049 100644 --- a/src/pages/events/logs/index.tsx +++ b/src/pages/events/logs/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent, ref, onMounted, onUnmounted, nextTick } from 'vue'; +import { defineComponent, ref, nextTick } from 'vue'; import { Button, Input, @@ -12,7 +12,7 @@ import { } from 'ant-design-vue'; import { useLogModel, ACTION_TYPE_OPTIONS, ACTION_SOURCE_OPTIONS } from './model/useLogModel'; 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 './index.module.less'; @@ -29,15 +29,17 @@ const LogContentCell = defineComponent({ if (el) setOverflow(el.scrollHeight > el.clientHeight); }; let observer: ResizeObserver | null = null; - onMounted(() => { - nextTick(checkOverflow); + useEffect(() => { const el = textRef.value; - if (el) { - observer = new ResizeObserver(checkOverflow); - observer.observe(el); - } - }); - onUnmounted(() => observer?.disconnect()); + if (!el) return; + nextTick(checkOverflow); + observer = new ResizeObserver(checkOverflow); + observer.observe(el); + return () => { + observer?.disconnect(); + observer = null; + }; + }, [() => textRef.value]); return () => { const raw = props.text; return ( diff --git a/src/pages/finance/payments/index.tsx b/src/pages/finance/payments/index.tsx index dfe6a4f..a115760 100644 --- a/src/pages/finance/payments/index.tsx +++ b/src/pages/finance/payments/index.tsx @@ -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 { usePaymentsModel } from './model/usePaymentsModel'; import { useContainerSize } from '@/hooks'; @@ -24,10 +24,6 @@ export default defineComponent({ const { containerRef, height } = useContainerSize({ headerOffset: 55 }); - onMounted(() => { - handleSearch(); - }); - return () => (
@@ -101,9 +97,9 @@ export default defineComponent({
`共 ${total} 条`} onChange={handlePageChange} diff --git a/src/pages/finance/payments/model/usePaymentsModel.ts b/src/pages/finance/payments/model/usePaymentsModel.ts index 71d08f6..a84e3c9 100644 --- a/src/pages/finance/payments/model/usePaymentsModel.ts +++ b/src/pages/finance/payments/model/usePaymentsModel.ts @@ -1,5 +1,5 @@ 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 dayjs from 'dayjs'; @@ -40,13 +40,50 @@ export function usePaymentsModel() { { delay: 300 }, ); - // ===== 汇总 ===== - const [summary, setSummary] = useState({ totalAmount: '0.00', totalRefundAmount: '0.00' }); + // ===== 分页 ===== + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); - // ===== 表格状态 ===== - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); + // ===== 构建 API 查询参数 ===== + const buildQueryParams = (): PaymentFlowQueryParams => { + const params: PaymentFlowQueryParams = { + 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) ===== 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 () => { - 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 handleSearch = () => fetchList(); const handleReset = useThrottleFn(() => { filterForm.timeRange = null; filterForm.nickname = ''; filterForm.phone = ''; filterForm.type = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setDataSource([]); - setSummary({ totalAmount: '0.00', totalRefundAmount: '0.00' }); - setTimeout(() => handleSearch(), 350); + pagination.reset(); + setTimeout(fetchList, 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { - setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); + pagination.setCurrent(page); + if (pageSize !== (pagination as any).pageSize.value) { + pagination.setPageSize(pageSize); + } + setTimeout(fetchList, 0); }; return { diff --git a/src/pages/finance/reports/index.tsx b/src/pages/finance/reports/index.tsx index af30a0a..19c8968 100644 --- a/src/pages/finance/reports/index.tsx +++ b/src/pages/finance/reports/index.tsx @@ -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 * as echarts from 'echarts'; import { useReportsModel } from './model/useReportsModel'; +import { useEffect } from '@/hooks'; import styles from './index.module.less'; /** @@ -140,23 +141,23 @@ export default defineComponent({ chartInstance?.resize(); }; - onMounted(() => { - nextTick(() => { - renderChart(); - }); + // ECharts 挂载/销毁 + 窗口 resize 监听 + useEffect(() => { + const el = chartRef.value; + if (!el) return; + nextTick(() => renderChart()); window.addEventListener('resize', handleResize); - }); + return () => { + window.removeEventListener('resize', handleResize); + chartInstance?.dispose(); + chartInstance = null; + }; + }, [() => chartRef.value]); - onBeforeUnmount(() => { - window.removeEventListener('resize', handleResize); - chartInstance?.dispose(); - chartInstance = null; - }); - - // 时间范围切换时刷新图表(接入 API 后生效) - watch(range, () => { + // 时间范围切换时刷新图表 + useEffect(() => { renderChart(); - }); + }, [range]); return () => { /** 根据增长率构建箭头 + 颜色(正值 ↑ 绿,负值 ↓ 红,null 时固定文本) */ diff --git a/src/pages/finance/wallet/index.tsx b/src/pages/finance/wallet/index.tsx index 9c58968..550a0dd 100644 --- a/src/pages/finance/wallet/index.tsx +++ b/src/pages/finance/wallet/index.tsx @@ -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 { WalletOutlined, LockOutlined, RiseOutlined } from '@ant-design/icons-vue'; import { useWalletModel } from './model/useWalletModel'; @@ -68,10 +68,6 @@ export default defineComponent({ }, ]; - onMounted(() => { - handleSearch(); - }); - return () => { const statCards: StatCardItem[] = [ { @@ -206,9 +202,9 @@ export default defineComponent({
`共 ${total} 条`} onChange={handlePageChange} diff --git a/src/pages/finance/wallet/model/useWalletModel.ts b/src/pages/finance/wallet/model/useWalletModel.ts index 437496a..f98661e 100644 --- a/src/pages/finance/wallet/model/useWalletModel.ts +++ b/src/pages/finance/wallet/model/useWalletModel.ts @@ -1,5 +1,12 @@ 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'; // ============================================================ @@ -58,17 +65,58 @@ export function useWalletModel() { { delay: 300 }, ); - // ===== 顶部统计(汇总值从接口返回的第一条记录中提取) ===== - const [summary, setSummary] = useState({ - totalBalance: '0.00', - frozenAmount: '0.00', - totalWithdraw: '0.00', + // ===== 分页 ===== + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); + + // ===== 构建 API 查询参数 ===== + 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 [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); + const listData = computed(() => (data.value as any)?.data); + const dataSource = computed(() => listData.value?.list || []); + + // 汇总:从第一条记录提取 + 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(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 () => { - 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 handleSearch = () => fetchList(); const handleReset = useThrottleFn(() => { filterForm.nickname = ''; filterForm.phone = ''; filterForm.minBalance = ''; filterForm.maxBalance = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setDataSource([]); - setSummary({ totalBalance: '0.00', frozenAmount: '0.00', totalWithdraw: '0.00' }); - setTimeout(() => handleSearch(), 350); + pagination.reset(); + setTimeout(fetchList, 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { - setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); + pagination.setCurrent(page); + if (pageSize !== (pagination as any).pageSize.value) { + pagination.setPageSize(pageSize); + } + setTimeout(fetchList, 0); }; const handleViewDetail = (record: any) => { diff --git a/src/pages/finance/withdraw/index.tsx b/src/pages/finance/withdraw/index.tsx index 233fa1b..e0aacd8 100644 --- a/src/pages/finance/withdraw/index.tsx +++ b/src/pages/finance/withdraw/index.tsx @@ -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 { useWithdrawModel, renderAuditStatus, renderPayStatus } from './model/useWithdrawModel'; import { useContainerSize } from '@/hooks'; @@ -82,11 +82,6 @@ export default defineComponent({ }, ]; - // 首次进入自动加载 - onMounted(() => { - handleSearch(); - }); - return () => (
{/* ===== 筛选区 ===== */} @@ -175,9 +170,9 @@ export default defineComponent({ {/* 独立分页,右下方 */}
`共 ${total} 条`} onChange={handlePageChange} diff --git a/src/pages/finance/withdraw/model/useWithdrawModel.ts b/src/pages/finance/withdraw/model/useWithdrawModel.ts index a68e784..7239a2f 100644 --- a/src/pages/finance/withdraw/model/useWithdrawModel.ts +++ b/src/pages/finance/withdraw/model/useWithdrawModel.ts @@ -1,6 +1,13 @@ import { computed, reactive, toRef, Ref, h } from 'vue'; 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'; // ============================================================ @@ -93,10 +100,41 @@ export function useWithdrawModel() { { delay: 300 }, ); - // ===== 表格状态 ===== - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); + // ===== 分页 ===== + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); + + // ===== 构建 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(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 = 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 handleSearch = () => fetchList(); /** 重置(重置后自动查询) */ const handleReset = useThrottleFn(() => { @@ -181,14 +188,16 @@ export function useWithdrawModel() { filterForm.realName = ''; filterForm.auditStatus = ''; filterForm.payStatus = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setDataSource([]); - setTimeout(() => handleSearch(), 350); + pagination.reset(); + setTimeout(fetchList, 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { - setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); + pagination.setCurrent(page); + if (pageSize !== (pagination as any).pageSize.value) { + pagination.setPageSize(pageSize); + } + setTimeout(fetchList, 0); }; /** 打开审核弹窗 */ diff --git a/src/pages/system/logs/index.tsx b/src/pages/system/logs/index.tsx index 265caf7..2054920 100644 --- a/src/pages/system/logs/index.tsx +++ b/src/pages/system/logs/index.tsx @@ -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 { useLogModel, ACTION_TYPE_OPTIONS, ACTION_TYPE_MAP } from './model/useLogModel'; import { useContainerSize } from '@/hooks'; @@ -35,11 +35,6 @@ export default defineComponent({ const { containerRef, height } = useContainerSize({ headerOffset: 55 }); - // 首次进入自动加载 - onMounted(() => { - handleSearch(); - }); - return () => (
{/* ===== 筛选区 ===== */} @@ -102,9 +97,9 @@ export default defineComponent({ {/* 独立分页 */}
`共 ${total} 条`} onChange={handlePageChange} diff --git a/src/pages/system/logs/model/useLogModel.ts b/src/pages/system/logs/model/useLogModel.ts index 6a269be..48c64f4 100644 --- a/src/pages/system/logs/model/useLogModel.ts +++ b/src/pages/system/logs/model/useLogModel.ts @@ -1,10 +1,15 @@ import { computed, reactive, toRef, Ref } from 'vue'; 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 { getOperationLogList, type SysOperationLogVO, type OperationLogQueryParams, + type PageData, + type ApiResult, } from './services'; // ============================================================ @@ -49,33 +54,14 @@ export function useLogModel() { { delay: 300 }, ); - // ===== 表格状态 ===== - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - 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() !== '' - ); - }); + // ===== 分页 ===== + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); // ===== 构建 API 查询参数 ===== const buildQueryParams = (): OperationLogQueryParams => { const params: OperationLogQueryParams = { - page: String(pagination.value.current), - limit: String(pagination.value.pageSize), + page: String((pagination as any).current.value), + limit: String((pagination as any).pageSize.value), }; // 时间范围 → dateBegin / dateEnd @@ -98,38 +84,62 @@ export function useLogModel() { return params; }; + // ===== 数据请求 ===== + const { + data, + loading, + run: fetchList, + } = useRequest>>( + () => 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 = 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 handleSearch = () => fetchList(); /** 重置(重置后自动查询) */ const handleReset = useThrottleFn(() => { filterForm.actionTimeRange = null; filterForm.actionType = ''; filterForm.searchOperator = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setDataSource([]); - setTimeout(() => handleSearch(), 350); + pagination.reset(); + setTimeout(fetchList, 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { - setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); + pagination.setCurrent(page); + if (pageSize !== (pagination as any).pageSize.value) { + pagination.setPageSize(pageSize); + } + setTimeout(fetchList, 0); }; return { diff --git a/src/pages/system/roles/components/UserListModal.tsx b/src/pages/system/roles/components/UserListModal.tsx index 19456cc..ab72ea7 100644 --- a/src/pages/system/roles/components/UserListModal.tsx +++ b/src/pages/system/roles/components/UserListModal.tsx @@ -143,7 +143,6 @@ export default defineComponent({ columns={columns} dataSource={dataSource.value} loading={loading.value} - scroll={{ y: 300 }} size="small" pagination={false} bordered @@ -164,7 +163,6 @@ export default defineComponent({ pageSize={pagination.value.pageSize} total={pagination.value.total} size="small" - showSizeChanger showTotal={(total: number) => `共 ${total} 条`} onChange={handlePageChange} /> diff --git a/src/pages/system/roles/index.tsx b/src/pages/system/roles/index.tsx index da6b2a0..9b614ff 100644 --- a/src/pages/system/roles/index.tsx +++ b/src/pages/system/roles/index.tsx @@ -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 type { ModalProps } from 'ant-design-vue'; import { useRoleModel } from './model/useRoleModel'; @@ -114,11 +114,6 @@ export default defineComponent({ }, ]; - // ===== 初始化 ===== - onMounted(() => { - handleSearch(); - }); - return () => (
{/* ===== 筛选区 ===== */} @@ -173,9 +168,9 @@ export default defineComponent({ {/* 分页 */}
`共 ${total} 条`} onChange={handlePageChange} diff --git a/src/pages/system/roles/model/useRoleModel.ts b/src/pages/system/roles/model/useRoleModel.ts index f9fbdb8..015db08 100644 --- a/src/pages/system/roles/model/useRoleModel.ts +++ b/src/pages/system/roles/model/useRoleModel.ts @@ -1,6 +1,9 @@ import { computed, reactive, toRef, Ref } from 'vue'; import { message } from 'ant-design-vue'; import { useState, useDebounce, useThrottleFn } from '@/hooks'; +import { useRequest } from '@/hooks/useRequest'; +import { useEffect } from '@/hooks/useEffect'; +import { usePagination } from '@/hooks/usePagination'; import { getRoleList, saveRole, @@ -8,6 +11,8 @@ import { deleteRole, type TournamentAdminRolePageVO, type RoleListQueryParams, + type PageData, + type ApiResult, } from './services'; // ============================================================ @@ -27,10 +32,38 @@ export function useRoleModel() { delay: 300, }); - // ===== 表格状态 ===== - const [loading, setLoading] = useState(false); - const [dataSource, setDataSource] = useState([]); - const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 }); + // ===== 分页 ===== + const pagination = usePagination({ defaultCurrent: 1, defaultPageSize: 10 }); + + // ===== 构建 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>>( + () => 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(false); @@ -55,53 +88,27 @@ export function useRoleModel() { { 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 isEdit = computed(() => editingRecord.value !== null); // ===== 方法 ===== - /** 查询(节流 500ms) */ - 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 handleSearch = () => fetchList(); /** 重置(重置后自动查询) */ const handleReset = useThrottleFn(() => { filterForm.name = ''; - setPagination({ current: 1, pageSize: 10, total: 0 }); - setDataSource([]); - setTimeout(() => handleSearch(), 350); + pagination.reset(); + setTimeout(fetchList, 350); }, 500); const handlePageChange = (page: number, pageSize: number) => { - setPagination({ ...pagination.value, current: page, pageSize }); - handleSearch(); + pagination.setCurrent(page); + if (pageSize !== (pagination as any).pageSize.value) { + pagination.setPageSize(pageSize); + } + setTimeout(fetchList, 0); }; /** 新增 */ @@ -146,7 +153,7 @@ export function useRoleModel() { if (res.code == 200) { message.success(isEdit.value ? '编辑成功' : '新增成功'); handleCloseForm(); - handleSearch(); + fetchList(); } else { message.error(res.msg || '操作失败'); } @@ -159,19 +166,16 @@ export function useRoleModel() { /** 删除 */ const handleDelete = useThrottleFn(async (record: any) => { - setLoading(true); try { const res = await deleteRole(record.roleId); if (res.code == 200) { - setDataSource(dataSource.value.filter((item: any) => item.roleId !== record.roleId)); message.success('删除成功'); + fetchList(); } else { message.error(res.msg || '删除失败'); } } catch { // 网络层已统一提示,不再重复 message.error - } finally { - setLoading(false); } }, 500); diff --git a/src/pages/system/users/components/UserFormModal.tsx b/src/pages/system/users/components/UserFormModal.tsx index 5eacad5..e1a2a3d 100644 --- a/src/pages/system/users/components/UserFormModal.tsx +++ b/src/pages/system/users/components/UserFormModal.tsx @@ -1,8 +1,8 @@ import { defineComponent, ref, reactive } from 'vue'; import { useEffect, useState } from '@/hooks'; import { Modal, Form, Input, Select, Button } from 'ant-design-vue'; +import { getRoleList, type TournamentAdminRolePageVO } from '../../roles/model/services'; import { - ROLE_FORM_OPTIONS, PASSWORD_PLACEHOLDER, realNameRules, phoneRules, @@ -55,12 +55,84 @@ export default defineComponent({ /** 编辑模式下是否处于"重置密码"状态 */ const [isResetting, setIsResetting] = useState(false); + // ---- 角色搜索状态 ---- + const roleKeyword = ref(''); + const [roleOptions, setRoleOptions] = useState<{ value: string; label: string }[]>([]); + const [roleSearchLoading, setRoleSearchLoading] = useState(false); + /** 手动防抖计时器 */ + let roleSearchTimer: ReturnType | null = null; + + /** 已加载的全量角色缓存 */ + const [allRoleOptions, setAllRoleOptions] = useState<{ value: string; label: string }[]>([]); + + // ---- 角色搜索 API 调用(用户主动搜索,带 loading) ---- + const doRoleSearch = async (keyword: string) => { + setRoleSearchLoading(true); + try { + const params: Record = { 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 初始化表单 */ const initFormFromRecord = (record: any) => { if (!record) { // 新增:预填默认密码 + 确认密码 Object.assign(formData, getAddForm()); setIsResetting(false); + roleKeyword.value = ''; + fetchInitialRoles(); return; } const fresh = getDefaultForm(); @@ -71,6 +143,22 @@ export default defineComponent({ roleId: record.roleId || undefined, }); 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 变化重置表单 */ @@ -82,6 +170,14 @@ export default defineComponent({ } }, [() => props.visible]); + /** 关闭时清理防抖计时器 */ + useEffect(() => { + if (!props.visible && roleSearchTimer) { + clearTimeout(roleSearchTimer); + roleSearchTimer = null; + } + }, [() => props.visible]); + /** 点击"重置密码" */ const handleResetPassword = () => { setIsResetting(true); @@ -172,9 +268,19 @@ export default defineComponent({ { + if (formData.roleId && roleOptions.value.length === 0) { + doRoleSearch(formData.roleId); + } + }} style={{ width: '200px' }} /> diff --git a/src/pages/system/users/controller.ts b/src/pages/system/users/controller.ts index c13ca64..de0a6e1 100644 --- a/src/pages/system/users/controller.ts +++ b/src/pages/system/users/controller.ts @@ -14,14 +14,6 @@ import { isChinaPhone, isValidPassword, isValidRealName } from '@/utils/form'; /** 密码占位符(写死长度,避免暴露真实密码长度) */ 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; - // ============================================================ // 真实姓名规则 // ============================================================ diff --git a/src/pages/system/users/index.tsx b/src/pages/system/users/index.tsx index 0eb6caf..c3d476b 100644 --- a/src/pages/system/users/index.tsx +++ b/src/pages/system/users/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent, onMounted } from 'vue'; +import { defineComponent } from 'vue'; import { Button, Input, @@ -10,7 +10,7 @@ import { Pagination, message, } 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 { useState, useThrottleFn, useContainerSize } from '@/hooks'; import UserFormModal from './components/UserFormModal'; @@ -60,6 +60,7 @@ export default defineComponent({ dataSource, columns, pagination, + roleOptions, handleSearch, handleReset, handlePageChange, @@ -175,11 +176,6 @@ export default defineComponent({ }, ]; - // ===== 初始化 ===== - onMounted(() => { - handleSearch(); - }); - return () => (
{/* ===== 筛选区 ===== */} @@ -198,7 +194,7 @@ export default defineComponent({