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
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:
1 parent
3e3bd02583
commit
dec9fd96e3
4 files changed
+194
-15
No files matched your search
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, [])
|
||||
})
|
||||
Reference in new issue
Block a user