Files
pj_admin_cloud/src/views/wallet/wallet-qb/useWalletQbUsers.ts
T
dzc 431371a13c
build / task (16.x, npm, macOS-latest) (push) Canceled after 0s
build / task (16.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, npm, windows-latest) (push) Canceled after 0s
build / task (16.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (16.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (16.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (16.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, yarn, windows-latest) (push) Canceled after 0s
build / task (18.x, npm, macOS-latest) (push) Canceled after 0s
build / task (18.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, npm, windows-latest) (push) Canceled after 0s
build / task (18.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (18.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (18.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (18.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, yarn, windows-latest) (push) Canceled after 0s
build / task (20.x, npm, macOS-latest) (push) Canceled after 0s
build / task (20.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, npm, windows-latest) (push) Canceled after 0s
build / task (20.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (20.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (20.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (20.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, yarn, windows-latest) (push) Canceled after 0s
feat:余额区间查询
2026-11-06 14:40:08 +08:00

185 lines
8.5 KiB
TypeScript

import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue"
import { fetchQbUsers, qbUsersTotal } from "/@/api/wallet/wallet-manage"
import type { WalletQbUserQuery, WalletQbUserRow, WalletQbUserTotal } from "/@/api/wallet/wallet-manage"
import { useMessage } from "/@/hooks/message"
import { formatDate } from "/@/utils/formatTime"
/** 金额输入以元展示,空字符串表示不限;请求使用整数分,避免浮点数比较金额。 */
export function parseBalanceYuan(value: string | number | undefined): number | undefined {
const input = String(value ?? "").trim()
if (!input) return undefined
if (!/^-?(?:\d+(?:\.\d{0,2})?|\.\d{1,2})$/.test(input)) throw new Error("余额最多输入两位小数")
// 先拼接整数分再转换数值,大额输入也不经过小数乘法,避免二进制浮点丢失一分钱。
const [integer, fraction = ""] = input.replace(/^-/, "").split(".")
const cents = Number(`${integer || "0"}${fraction.padEnd(2, "0")}`) * (input.startsWith("-") ? -1 : 1)
if (!Number.isSafeInteger(cents)) throw new Error("余额超出支持范围")
return cents
}
/** 用户汇总页独立持有筛选快照、取消句柄和请求序号,旧响应不能覆盖新查询。 */
export function useWalletQbUsers() {
/** 搜索表单是待提交条件;翻页、排序、统计与导出只使用已应用条件。 */
const state = reactive({
queryForm: {
walletRecordMonth: formatDate(new Date(), "YYYY-mm"),
userId: undefined as string | number | undefined,
walletTypeIdList: [] as Array<string | number>,
deptIds: [] as Array<string | number>,
isInclude: true,
minBalanceYuan: "",
maxBalanceYuan: "",
},
dataList: [] as WalletQbUserRow[],
pagination: { current: 1, size: 20, total: 0, pageSizes: [20, 50, 100, 200], layout: "total, sizes, prev, pager, next, jumper" },
loading: false,
})
/** 最近一次提交的查询快照,金额已转成整数分。 */
const appliedQuery = ref<WalletQbUserQuery>({ walletRecordMonth: state.queryForm.walletRecordMonth })
/** 服务端排序字段白名单,取消排序后由后端使用稳定的用户顺序。 */
const sort = reactive({ ascs: "", descs: "" })
const pageError = ref("")
const balanceStatisticsLoading = ref(false)
const balanceStatisticsLoaded = ref(false)
const balanceStatisticsError = ref("")
const balanceStatistics = reactive<Partial<WalletQbUserTotal>>({})
/** 列表与统计独立取消,改变筛选或卸载时释放浏览器等待。 */
let listController: AbortController | undefined
let statisticsController: AbortController | undefined
let listSequence = 0
let statisticsSequence = 0
let disposed = false
/** 构建可独立使用的筛选快照,单边、零和负数边界均保留。 */
const buildQuery = (): WalletQbUserQuery => {
const { walletRecordMonth, userId, walletTypeIdList, deptIds, isInclude, minBalanceYuan, maxBalanceYuan } = state.queryForm
const minBalance = parseBalanceYuan(minBalanceYuan)
const maxBalance = parseBalanceYuan(maxBalanceYuan)
if (minBalance !== undefined && maxBalance !== undefined && minBalance > maxBalance) throw new Error("最低余额不能大于最高余额")
return { walletRecordMonth, userId, walletTypeIdList: [...walletTypeIdList], deptIds: [...deptIds], isInclude, minBalance, maxBalance }
}
/** 将区间错误展示在金额输入旁,提交时也阻止无效请求。 */
const rangeError = computed(() => {
try { buildQuery(); return "" } catch (error: any) { return error.message }
})
/** 清除旧统计,避免月份或区间改变后仍展示上一范围的数字。 */
const clearStatistics = () => {
statisticsSequence++
statisticsController?.abort()
statisticsController = undefined
balanceStatisticsLoading.value = false
balanceStatisticsLoaded.value = false
balanceStatisticsError.value = ""
for (const key of Object.keys(balanceStatistics)) delete balanceStatistics[key as keyof WalletQbUserTotal]
}
/** 查询已应用筛选下的当前页;成功、失败和取消均受请求序号约束。 */
const queryPage = async () => {
listController?.abort()
const controller = new AbortController()
listController = controller
const sequence = ++listSequence
state.loading = true
pageError.value = ""
try {
const query = JSON.parse(JSON.stringify(appliedQuery.value))
const response = await fetchQbUsers({ ...query, ...sort, current: state.pagination.current, size: state.pagination.size }, controller.signal)
if (disposed || sequence !== listSequence || controller.signal.aborted) return
if (response.code !== undefined && response.code !== 0) throw new Error(response.msg || "余额查询失败")
state.dataList = response.data?.records ?? []
state.pagination.total = response.data?.total ?? 0
} catch (error: any) {
if (disposed || sequence !== listSequence || controller.signal.aborted) return
state.dataList = []
state.pagination.total = 0
pageError.value = error?.msg || error?.message || "余额查询失败,请重试"
} finally {
if (sequence === listSequence && !disposed) {
state.loading = false
listController = undefined
}
}
}
/** 提交搜索条件,范围变更后清空旧统计;区间无效时不发出请求。 */
const getDataList = async () => {
let query: WalletQbUserQuery
try { query = buildQuery() } catch (error: any) { useMessage().error(error.message); return }
// 同一范围重新查询也可能有新流水,旧统计必须失效,避免刷新后金额新旧混用。
clearStatistics()
appliedQuery.value = query
state.pagination.current = 1
state.dataList = []
await queryPage()
}
/** 翻页保持已查询范围,不隐式应用尚未提交的表单输入。 */
const currentChangeHandle = (current: number) => {
if (current === state.pagination.current) return
state.pagination.current = current
return queryPage()
}
/** 改变每页人数时回到第一页。 */
const sizeChangeHandle = (size: number) => {
state.pagination.size = size
state.pagination.current = 1
return queryPage()
}
/** 四个金额列均由后端对全部匹配用户排序,切换方向后回到第一页。 */
const handleBalanceSortChange = ({ prop, order }: { prop?: string; order: "ascending" | "descending" | null }) => {
const fields: Record<string, string> = { balance: "balance", eatBalance: "eat_balance", payBalance: "pay_balance", topupBalance: "topup_balance" }
const field = fields[prop || ""]
sort.ascs = field && order === "ascending" ? field : ""
sort.descs = field && order === "descending" ? field : ""
state.pagination.current = 1
return queryPage()
}
/** 全量统计按需计算,翻页不重复扫描历史余额;统计失败可直接重试。 */
const viewBalanceStatistics = async () => {
if (balanceStatisticsLoading.value || state.loading || disposed) return
const sequence = ++statisticsSequence
const controller = new AbortController()
statisticsController = controller
balanceStatisticsLoading.value = true
balanceStatisticsLoaded.value = false
balanceStatisticsError.value = ""
try {
const query = JSON.parse(JSON.stringify(appliedQuery.value))
const response = await qbUsersTotal(query, controller.signal)
if (disposed || sequence !== statisticsSequence || controller.signal.aborted) return
if (response.code !== undefined && response.code !== 0) throw new Error(response.msg || "统计失败")
Object.assign(balanceStatistics, response.data)
balanceStatisticsLoaded.value = true
} catch (error: any) {
if (disposed || sequence !== statisticsSequence || controller.signal.aborted) return
balanceStatisticsError.value = error?.msg || error?.message || "统计失败,请重试"
} finally {
if (sequence === statisticsSequence && !disposed) {
balanceStatisticsLoading.value = false
statisticsController = undefined
}
}
}
/** 导出与当前页面使用同一范围快照,不带列表分页。 */
const getAppliedQuery = () => JSON.parse(JSON.stringify({ ...appliedQuery.value, ...sort }))
/** 组件卸载时取消在途请求,防止旧响应写入或弹出错误。 */
const dispose = () => {
disposed = true
listSequence++
listController?.abort()
clearStatistics()
}
onMounted(getDataList)
onBeforeUnmount(dispose)
return { state, appliedQuery, rangeError, pageError, getDataList, currentChangeHandle, sizeChangeHandle, handleBalanceSortChange,
viewBalanceStatistics, clearStatistics, balanceStatistics, balanceStatisticsLoading, balanceStatisticsLoaded, balanceStatisticsError,
getAppliedQuery, dispose }
}