111
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

This commit is contained in:
dzc committed 2026-10-23 14:38:26 +08:00
1 parent 3e3bd02583
commit dec9fd96e3
4 files changed
+194 -15

No files matched your search

+2 -1
View File
@@ -43,11 +43,12 @@ export function fetchList(query?: Object) {
})
}
export function qbTotal(data: any) {
export function qbTotal(data: any, signal?: AbortSignal) {
return request({
url: "/wallet/wallet-manage/qb-total",
method: "post",
data: data,
signal,
})
}
@@ -6,7 +6,6 @@ const ignoredFields = new Set(["current", "size", "descs", "ascs", "ids", "selec
/** 在点击时复制表单,避免分页、导出勾选或后续表单编辑污染统计。 */
export function snapshotStatisticsQuery(form: HardOrderStatisticsQuery): HardOrderStatisticsQuery {
const query = JSON.parse(JSON.stringify(Object.fromEntries(Object.entries(form).filter(([key]) => !ignoredFields.has(key)))))
let hasTimeRange = false
for (const [key, label] of [["createTimeList", "创建时间"], ["payTimeList", "支付时间"]]) {
const range = query[key]
if (range == null || (Array.isArray(range) && range.length === 0)) continue
@@ -18,12 +17,7 @@ export function snapshotStatisticsQuery(form: HardOrderStatisticsQuery): HardOrd
if (!start.isValid() || !end.isValid() || end.isBefore(start)) {
throw new Error(`${label}开始时间不能晚于结束时间`)
}
if (end.isAfter(start.add(3, "month").endOf("day"))) {
throw new Error(`${label}区间不能超过三个月,请缩小查询范围`)
}
hasTimeRange = true
}
if (!hasTimeRange) throw new Error("统计需选择创建时间或支付时间,单个区间不能超过三个月")
return query
}
+39 -8
View File
@@ -60,7 +60,7 @@
@queryTable="getDataList"
>
<el-tooltip class="item" effect="dark" content="统计" placement="top">
<el-button icon="DataLine" circle @click="viewBalanceStatistics" />
<el-button icon="DataLine" circle :loading="balanceStatisticsLoading" @click="viewBalanceStatistics" />
</el-tooltip>
</right-toolbar>
</div>
@@ -120,8 +120,13 @@
</DraggableDepTree>
</div>
<el-drawer v-model="showDrawer" @closed="drawerClose">
<el-table :data="[balanceStatistics]" style="width: 100%">
<el-drawer v-model="showDrawer" @close="drawerClose">
<el-table
v-loading="balanceStatisticsLoading"
:data="balanceStatisticsLoaded ? [balanceStatistics] : []"
:empty-text="balanceStatisticsLoading ? '统计中…' : '暂无统计数据'"
style="width: 100%"
>
<el-table-column align="center" label="余额统计">
<el-table-column align="center" prop="total" label="总余额">
<template #default="{ row }">
@@ -159,6 +164,7 @@ import { useMessage, useMessageBox } from "/@/hooks/message"
import { useDict } from "/@/hooks/dict"
import { displayPrice } from "/@/utils/formatMoney"
import { formatDate } from "/@/utils/formatTime"
import { onBeforeUnmount } from "vue"
const { wallet_type, yes_no_type } = useDict("wallet_type", "yes_no_type")
// 定义变量内容
@@ -171,7 +177,10 @@ const selectObjs = ref([]) as any
const multiple = ref(true)
const showDrawer = ref(false) // 余额统计抽屉开关
const balanceStatisticsLoading = ref(false)
const balanceStatisticsLoaded = ref(false)
const balanceStatistics = reactive<any>({})
let balanceStatisticsController: AbortController | undefined
let balanceStatisticsSequence = 0
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
@@ -198,28 +207,50 @@ const resetQuery = () => {
// 余额统计
const viewBalanceStatistics = async () => {
showDrawer.value = true
// 慢查询在途时重复点击不再发送统计请求。
if (balanceStatisticsLoading.value) return
balanceStatisticsLoading.value = true
balanceStatisticsLoaded.value = false
const sequence = ++balanceStatisticsSequence
const controller = new AbortController()
balanceStatisticsController = controller
try {
const { code, data } = await qbTotal({ ...state.queryForm })
// 数组也取快照,避免筛选变化影响本次查询参数。
const query = JSON.parse(JSON.stringify(state.queryForm))
const { code, data, msg } = await qbTotal(query, controller.signal)
if (sequence !== balanceStatisticsSequence || controller.signal.aborted) return
if (code == 0) {
Object.assign(balanceStatistics, data)
balanceStatisticsLoaded.value = true
return
}
return useMessage().error(data.msg || "获取数据失败")
return useMessage().error(msg || data?.msg || "获取数据失败")
} catch (error: any) {
useMessage().error(error.msg)
if (sequence !== balanceStatisticsSequence || controller.signal.aborted) return
useMessage().error(error?.msg || error?.message || "获取数据失败")
} finally {
balanceStatisticsLoading.value = false
if (sequence === balanceStatisticsSequence) {
balanceStatisticsLoading.value = false
balanceStatisticsController = undefined
}
}
}
// 关闭抽屉
const drawerClose = () => {
// 在关闭开始时取消,避免关闭动画结束前重开抽屉被旧响应覆盖。
balanceStatisticsSequence++
balanceStatisticsController?.abort()
balanceStatisticsController = undefined
balanceStatisticsLoading.value = false
balanceStatisticsLoaded.value = false
for (const key in balanceStatistics) {
balanceStatistics[key] = 0
delete balanceStatistics[key]
}
}
onBeforeUnmount(drawerClose)
// 导出excel
const { exportProgressRef, exportTaskConfig, submitExport, handleExportSuccess, handleExportFailure } = useBatchExport({
progressUrl: "/wallet/async-task/progress", redisKey: "wallet:export:qb",
+153
View File
@@ -0,0 +1,153 @@
const { test } = require("node:test")
const assert = require("node:assert/strict")
const fs = require("node:fs")
const path = require("node:path")
const ts = require("typescript")
const vue = require("vue")
const { parse, compileScript, compileTemplate } = require("@vue/compiler-sfc")
const root = path.join(__dirname, "../src")
const filename = path.join(root, "views/wallet/wallet-qb/index.vue")
const descriptor = parse(fs.readFileSync(filename, "utf8"), { filename }).descriptor
const source = compileScript(descriptor, { id: "wallet-qb-statistics-test" }).content
function load(source, mocks) {
const compiled = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020, esModuleInterop: true },
}).outputText
const mod = { exports: {} }
const autoImports = ["ref", "reactive", "onMounted"]
new Function("module", "exports", "require", ...autoImports, compiled)(
mod, mod.exports, id => id in mocks ? mocks[id] : require(id), ...autoImports.map(key => vue[key]),
)
return mod.exports
}
function mount() {
const requests = []
const lists = []
const errors = []
const table = load(fs.readFileSync(path.join(root, "hooks/table.ts"), "utf8"), {
"element-plus": { ElMessage: { error: message => errors.push(message) } },
"/@/utils/other": {}, "../utils/formatTime": {},
})
const page = load(source, {
"/@/hooks/table": table,
"/@/api/wallet/wallet-manage": {
fetchListJoinQb: query => { lists.push(query); return Promise.resolve({ data: { records: [], total: 100 } }) },
qbTotal: (query, signal) => new Promise((resolve, reject) => requests.push({ query, signal, resolve, reject })),
},
"/@/hooks/batchExport": { useBatchExport: () => ({}) },
"/@/components/AsyncTaskProgress.vue": {},
"/@/hooks/message": { useMessage: () => ({ error: message => errors.push(message) }) },
"/@/hooks/dict": { useDict: () => ({}) },
"/@/utils/formatMoney": {},
"/@/utils/formatTime": { formatDate: () => "2026-10" },
})
const renderer = vue.createRenderer({
createElement: () => ({}), createText: () => ({}), createComment: () => ({}),
setText() {}, setElementText() {}, patchProp() {}, insert() {}, remove() {}, parentNode: () => null, nextSibling: () => null,
})
let state
const app = renderer.createApp({
setup() { state = page.default.setup({}, { expose() {} }); return () => null },
})
app.mount({})
return { state, requests, lists, errors, close: () => app.unmount() }
}
const settle = () => new Promise(resolve => setImmediate(resolve))
const result = total => ({ code: 0, data: { total, eatTotal: total, payTotal: 0, topupTotal: 0 } })
test("列表首次加载、筛选和翻页均不触发全量余额统计", async () => {
const page = mount()
try {
await settle()
assert.equal(page.lists.length, 1)
await page.state.getDataList()
page.state.currentChangeHandle(2)
page.state.sizeChangeHandle(20)
await settle()
assert.equal(page.lists.length, 4)
assert.equal(page.requests.length, 0)
} finally { page.close() }
})
test("统计请求在途去重,数组筛选冻结且加载完成前不展示旧统计", async () => {
const page = mount()
try {
Object.assign(page.state.state.queryForm, { deptIds: ["11"], walletTypeIdList: ["9007199254740993999"], isInclude: false })
page.state.viewBalanceStatistics()
page.state.viewBalanceStatistics()
assert.equal(page.requests.length, 1)
assert.equal(page.state.balanceStatisticsLoading.value, true)
assert.equal(page.state.balanceStatisticsLoaded.value, false)
page.state.state.queryForm.deptIds.push("12")
assert.deepEqual(page.requests[0].query, {
walletRecordMonth: "2026-10", deptIds: ["11"], walletTypeIdList: ["9007199254740993999"], isInclude: false,
})
page.requests[0].resolve(result(1200))
await settle()
assert.equal(page.state.balanceStatistics.total, 1200)
assert.equal(page.state.balanceStatisticsLoaded.value, true)
assert.equal(page.state.balanceStatisticsLoading.value, false)
} finally { page.close() }
})
test("关闭和重开立即取消旧请求,旧成功或失败不污染新统计与 loading", async () => {
const page = mount()
try {
page.state.viewBalanceStatistics()
page.state.drawerClose()
assert.equal(page.requests[0].signal.aborted, true)
page.state.state.queryForm.walletRecordMonth = "2026-09"
page.state.viewBalanceStatistics()
page.requests[0].reject(new Error("旧请求失败"))
await settle()
assert.equal(page.state.balanceStatisticsLoading.value, true)
assert.deepEqual(page.errors, [])
page.state.drawerClose()
page.state.viewBalanceStatistics()
page.requests[2].resolve(result(3000))
await settle()
page.requests[1].resolve(result(1000))
await settle()
assert.equal(page.state.balanceStatistics.total, 3000)
assert.equal(page.state.balanceStatisticsLoaded.value, true)
assert.equal(page.state.balanceStatisticsLoading.value, false)
} finally { page.close() }
})
test("网络失败展示可读错误并可重试,卸载取消在途请求", async () => {
const page = mount()
page.state.viewBalanceStatistics()
page.requests[0].reject(new Error("查询超时"))
await settle()
assert.deepEqual(page.errors, ["查询超时"])
assert.equal(page.state.balanceStatisticsLoaded.value, false)
assert.equal(page.state.balanceStatisticsLoading.value, false)
page.state.viewBalanceStatistics()
assert.equal(page.requests.length, 2)
page.close()
assert.equal(page.requests[1].signal.aborted, true)
page.requests[1].resolve(result(2000))
await settle()
assert.equal(page.state.balanceStatisticsLoaded.value, false)
})
test("统计 API 保留路径与 POST 参数并传递取消信号,实际 Vue 模板可编译", async () => {
const requests = []
const api = load(fs.readFileSync(path.join(root, "api/wallet/wallet-manage.ts"), "utf8"), {
"/@/utils/request": options => { requests.push(options); return Promise.resolve({}) },
"/@/api/asyncTask": {},
})
const controller = new AbortController()
const query = { walletRecordMonth: "2026-10", deptIds: ["11"] }
await api.qbTotal(query, controller.signal)
assert.equal(requests[0].url, "/wallet/wallet-manage/qb-total")
assert.equal(requests[0].method, "post")
assert.equal(requests[0].data, query)
assert.equal(requests[0].signal, controller.signal)
const script = compileScript(descriptor, { id: "wallet-qb-statistics-test" })
assert.deepEqual(compileTemplate({ source: descriptor.template.content, filename, id: "wallet-qb-statistics-test", compilerOptions: { bindingMetadata: script.bindings } }).errors, [])
})