refactor(frontend): 金额正则/分页/抽屉状态/行编辑与下拉收口
- 金额形态正则收进 utils/validate.ts 单点导出(此前四页各写,精度 调整漏改一页即「A 页拦得住 B 页放行后端 400」或历史行无法编辑) - params 版本/历史两抽屉的手写分页五件套换 usePagedList(白得请求 序号防竞态——快速翻页旧响应覆盖新页的手写坑不再复现) - 业务抽屉三份 id 状态收为 drawerRowId computed(单一状态源,消除 「标题 A 业务、留痕表 B 业务」的失同步面) - useEditingRow(三页逐字相同的 computed)与 useDropdownOptions (取数失败不阻断列表的骨架)收口,映射函数由调用方给出 - e2e 冒烟:基础数据 13 步 + 业务全链路 16 步全过 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
60f6af6494
commit
0ef3adec21
8 files changed
+117
-98
No files matched your search
@@ -0,0 +1,24 @@
|
|||||||
|
import { ref, type Ref } from 'vue'
|
||||||
|
|
||||||
|
import { handledError } from '@/utils/error'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下拉选项的加载状态:取数失败不阻断列表使用(下拉为空 + 拦截器提示)。
|
||||||
|
*
|
||||||
|
* 业务/客户/人员等参照下拉在多个页面重复同一「try 取数 → map 成选项 →
|
||||||
|
* 失败吞掉」骨架,映射函数由调用方给出(各页字段不同),骨架收口单点。
|
||||||
|
*/
|
||||||
|
export function useDropdownOptions<T>(fetch: () => Promise<T[]>): {
|
||||||
|
options: Ref<T[]>
|
||||||
|
load: () => Promise<void>
|
||||||
|
} {
|
||||||
|
const options = ref([]) as Ref<T[]>
|
||||||
|
async function load(): Promise<void> {
|
||||||
|
try {
|
||||||
|
options.value = await fetch()
|
||||||
|
} catch (error) {
|
||||||
|
handledError(error) // 下拉为空不阻断列表使用
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { options, load }
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { computed, type Ref } from 'vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑中的行:editingId 非空时从当前页记录里找出行(回显兜底用)。
|
||||||
|
*
|
||||||
|
* 回款/发货/人员三页此前各持一份逐字相同的 computed(行对象上要展示
|
||||||
|
* 「编辑中」的派生信息,如业务编号、停用岗位回显),收口后口径一处维护。
|
||||||
|
*/
|
||||||
|
export function useEditingRow<T extends { id: number }>(
|
||||||
|
records: Ref<T[]>,
|
||||||
|
editingId: Ref<number | null>,
|
||||||
|
) {
|
||||||
|
return computed(() =>
|
||||||
|
editingId.value === null ? null : (records.value.find((r) => r.id === editingId.value) ?? null),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -12,6 +12,21 @@ export const PASSWORD_MAX_BYTES = 72
|
|||||||
|
|
||||||
const BYTE_HINT = '(中文一个字符约 3 字节)'
|
const BYTE_HINT = '(中文一个字符约 3 字节)'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金额形态正则(与后端 `schemas/business._check_amount` 同口径单点维护)。
|
||||||
|
*
|
||||||
|
* 「DECIMAL 列 ↔ 前端放行位数」规则此前在业务/发货/回款/人员四页各写一份
|
||||||
|
* (businesses 与 shipments 逐字重复,receipts/personnel 又各自内联两位小数
|
||||||
|
* 正则),后端精度调整时漏改任何一页,就会出现「A 页面拦得住、B 页面放行
|
||||||
|
* 后端 400」或「带精度历史行永远无法编辑」(fba8cfc 的教训)。
|
||||||
|
*/
|
||||||
|
/** 非负数字、最多两位小数(对齐 DECIMAL(14,2):金额/数量/成本/运费/工资) */
|
||||||
|
export const AMOUNT_PATTERN = /^(\d+)(\.\d{1,2})?$/
|
||||||
|
/** 非负数字、最多四位小数(对齐 DECIMAL(14,4):单价/指导价) */
|
||||||
|
export const PRICE_PATTERN = /^(\d+)(\.\d{1,4})?$/
|
||||||
|
/** 全零形态(数量「0 或 0.00」须拦:数量 >0 是明细行的硬约束) */
|
||||||
|
export const ZERO_PATTERN = /^0+(\.0+)?$/
|
||||||
|
|
||||||
/** 计算 UTF-8 字节数 */
|
/** 计算 UTF-8 字节数 */
|
||||||
export function passwordByteLength(value: string): number {
|
export function passwordByteLength(value: string): number {
|
||||||
return new TextEncoder().encode(value).length
|
return new TextEncoder().encode(value).length
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import AppPagination from '@/components/table/AppPagination.vue'
|
|||||||
import StatusTag from '@/components/table/StatusTag.vue'
|
import StatusTag from '@/components/table/StatusTag.vue'
|
||||||
import { useConfirmAction } from '@/composables/useConfirmAction'
|
import { useConfirmAction } from '@/composables/useConfirmAction'
|
||||||
import { usePagedList } from '@/composables/usePagedList'
|
import { usePagedList } from '@/composables/usePagedList'
|
||||||
|
import { useEditingRow } from '@/composables/useEditingRow'
|
||||||
|
import { AMOUNT_PATTERN } from '@/utils/validate'
|
||||||
import { handledError } from '@/utils/error'
|
import { handledError } from '@/utils/error'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,9 +109,7 @@ const form = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
/** 编辑中的行(用于停用岗位/离职经理的回显兜底) */
|
/** 编辑中的行(用于停用岗位/离职经理的回显兜底) */
|
||||||
const editingRow = computed(() =>
|
const editingRow = useEditingRow(records, editingId)
|
||||||
editingId.value === null ? null : records.value.find((r) => r.id === editingId.value) ?? null,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** 搜索下拉显示全部岗位(含停用,便于筛出仍绑定停用岗位的人员) */
|
/** 搜索下拉显示全部岗位(含停用,便于筛出仍绑定停用岗位的人员) */
|
||||||
const searchRoleOptions = computed(() =>
|
const searchRoleOptions = computed(() =>
|
||||||
@@ -139,7 +139,7 @@ const rules = computed<FormRules>(() => ({
|
|||||||
business_role_id: [{ required: true, message: '请选择岗位', trigger: 'change' }],
|
business_role_id: [{ required: true, message: '请选择岗位', trigger: 'change' }],
|
||||||
phone: [{ max: 20, message: '联系电话最长 20 个字符', trigger: 'blur' }],
|
phone: [{ max: 20, message: '联系电话最长 20 个字符', trigger: 'blur' }],
|
||||||
salary_standard: [
|
salary_standard: [
|
||||||
{ pattern: /^(\d+)(\.\d{1,2})?$/, message: '工资标准须为非负数字,最多两位小数', trigger: 'blur' },
|
{ pattern: AMOUNT_PATTERN, message: '工资标准须为非负数字,最多两位小数', trigger: 'blur' },
|
||||||
],
|
],
|
||||||
remark: [{ max: 255, message: '备注最长 255 个字符', trigger: 'blur' }],
|
remark: [{ max: 255, message: '备注最长 255 个字符', trigger: 'blur' }],
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import AppPagination from '@/components/table/AppPagination.vue'
|
|||||||
import { useConfirmAction } from '@/composables/useConfirmAction'
|
import { useConfirmAction } from '@/composables/useConfirmAction'
|
||||||
import { usePagedList } from '@/composables/usePagedList'
|
import { usePagedList } from '@/composables/usePagedList'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
|
import { AMOUNT_PATTERN, PRICE_PATTERN, ZERO_PATTERN } from '@/utils/validate'
|
||||||
import { handledError } from '@/utils/error'
|
import { handledError } from '@/utils/error'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,15 +102,6 @@ function personnelLabel(option: PersonnelOption): string {
|
|||||||
|
|
||||||
// ==================== 新增 / 编辑(含明细行编辑器) ====================
|
// ==================== 新增 / 编辑(含明细行编辑器) ====================
|
||||||
|
|
||||||
/** 金额/数量形态:非负数字、最多两位小数(纯形态校验,不涉及 float 运算) */
|
|
||||||
const AMOUNT_PATTERN = /^(\d+)(\.\d{1,2})?$/
|
|
||||||
// 单价类列(sale_price/guided_price)建表为 Numeric(14,4):后端 str 直出
|
|
||||||
// 会带最多 4 位小数(如 "5000.0000"),前端必须放行到 4 位——否则任何
|
|
||||||
// 带小数精度的历史记录在编辑时都会被前端永远拦下
|
|
||||||
const PRICE_PATTERN = /^(\d+)(\.\d{1,4})?$/
|
|
||||||
/** 全零形态:数量用它排除「>0」语义,不走 float 比较 */
|
|
||||||
const ZERO_PATTERN = /^0+(\.0+)?$/
|
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const editingId = ref<number | null>(null)
|
const editingId = ref<number | null>(null)
|
||||||
/** 单一状态源:编辑中 ⇔ editingId 非空(避免双状态漂移) */
|
/** 单一状态源:编辑中 ⇔ editingId 非空(避免双状态漂移) */
|
||||||
@@ -429,8 +421,11 @@ function personnelName(id: number): string {
|
|||||||
// ==================== 详情抽屉 ====================
|
// ==================== 详情抽屉 ====================
|
||||||
|
|
||||||
const drawerVisible = ref(false)
|
const drawerVisible = ref(false)
|
||||||
const drawerRowId = ref<number | null>(null)
|
|
||||||
const drawerRow = ref<BusinessItem | null>(null)
|
const drawerRow = ref<BusinessItem | null>(null)
|
||||||
|
// 单一状态源:drawerRowId 派生自 drawerRow(此前与 ownershipLogsId 三份并存、
|
||||||
|
// openDrawer 手工同步——新增写入路径漏更新其一就会出现「标题是 A 业务、
|
||||||
|
// 留痕表拉到 B 业务」且无报错)
|
||||||
|
const drawerRowId = computed(() => drawerRow.value?.id ?? null)
|
||||||
const drawerTitle = computed(() =>
|
const drawerTitle = computed(() =>
|
||||||
drawerRow.value ? `「${drawerRow.value.business_no}」· 业务详情` : '业务详情',
|
drawerRow.value ? `「${drawerRow.value.business_no}」· 业务详情` : '业务详情',
|
||||||
)
|
)
|
||||||
@@ -467,12 +462,10 @@ async function refreshDrawer(id: number): Promise<void> {
|
|||||||
|
|
||||||
function openDrawer(row: BusinessItem): void {
|
function openDrawer(row: BusinessItem): void {
|
||||||
drawerRow.value = row
|
drawerRow.value = row
|
||||||
drawerRowId.value = row.id
|
|
||||||
drawerVisible.value = true
|
drawerVisible.value = true
|
||||||
detail.value = null
|
detail.value = null
|
||||||
statusLogs.value = []
|
statusLogs.value = []
|
||||||
gp.value = null
|
gp.value = null
|
||||||
ownershipLogsId.value = row.id
|
|
||||||
ownershipLogPageNum.value = 1
|
ownershipLogPageNum.value = 1
|
||||||
void loadDrawerData(row.id)
|
void loadDrawerData(row.id)
|
||||||
void loadOwnershipLogs()
|
void loadOwnershipLogs()
|
||||||
@@ -491,7 +484,7 @@ const {
|
|||||||
onSizeChange: onOwnershipLogSizeChange,
|
onSizeChange: onOwnershipLogSizeChange,
|
||||||
} = usePagedList<businessApi.OwnershipLogItem, object>(
|
} = usePagedList<businessApi.OwnershipLogItem, object>(
|
||||||
(params) => {
|
(params) => {
|
||||||
const id = ownershipLogsId.value
|
const id = drawerRowId.value
|
||||||
// 抽屉未指定业务时兜底空数据,避免发出无意义请求
|
// 抽屉未指定业务时兜底空数据,避免发出无意义请求
|
||||||
return id === null
|
return id === null
|
||||||
? Promise.resolve({ list: [], total: 0, pageNum: 1, pageSize: 10 })
|
? Promise.resolve({ list: [], total: 0, pageNum: 1, pageSize: 10 })
|
||||||
@@ -500,8 +493,6 @@ const {
|
|||||||
() => ({}),
|
() => ({}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const ownershipLogsId = ref<number | null>(null)
|
|
||||||
|
|
||||||
/** 留痕展示:负责/协助的新旧人员(姓名随联表,ID 固化) */
|
/** 留痕展示:负责/协助的新旧人员(姓名随联表,ID 固化) */
|
||||||
function ownershipTransition(row: businessApi.OwnershipLogItem): string {
|
function ownershipTransition(row: businessApi.OwnershipLogItem): string {
|
||||||
const oldOwner = row.old_owner_name ?? '(无)'
|
const oldOwner = row.old_owner_name ?? '(无)'
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import type { ShipmentItem } from '@/api/shipments'
|
|||||||
import AppPagination from '@/components/table/AppPagination.vue'
|
import AppPagination from '@/components/table/AppPagination.vue'
|
||||||
import { useConfirmAction } from '@/composables/useConfirmAction'
|
import { useConfirmAction } from '@/composables/useConfirmAction'
|
||||||
import { usePagedList } from '@/composables/usePagedList'
|
import { usePagedList } from '@/composables/usePagedList'
|
||||||
|
import { AMOUNT_PATTERN } from '@/utils/validate'
|
||||||
|
import { useEditingRow } from '@/composables/useEditingRow'
|
||||||
|
import { useDropdownOptions } from '@/composables/useDropdownOptions'
|
||||||
import { handledError } from '@/utils/error'
|
import { handledError } from '@/utils/error'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,21 +66,17 @@ interface BusinessOption {
|
|||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const businessOptions = ref<BusinessOption[]>([])
|
// 业务下拉数据源:一页 200 条覆盖内部系统规模;超出由后端校验兜底
|
||||||
|
const { options: businessOptions, load: loadBusinessOptions } = useDropdownOptions<BusinessOption>(
|
||||||
async function loadBusinessOptions(): Promise<void> {
|
async () => {
|
||||||
try {
|
|
||||||
// 业务下拉数据源:一页 200 条覆盖内部系统规模;超出由后端校验兜底
|
|
||||||
const data = await businessApi.listBusinesses({ pageNum: 1, pageSize: 200 })
|
const data = await businessApi.listBusinesses({ pageNum: 1, pageSize: 200 })
|
||||||
businessOptions.value = data.list.map((b) => ({
|
return data.list.map((b) => ({
|
||||||
id: b.id,
|
id: b.id,
|
||||||
no: b.business_no,
|
no: b.business_no,
|
||||||
name: b.customer_name ?? '',
|
name: b.customer_name ?? '',
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
},
|
||||||
handledError(error) // 下拉为空不阻断列表使用
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function businessLabel(option: BusinessOption): string {
|
function businessLabel(option: BusinessOption): string {
|
||||||
return option.name ? `${option.no} · ${option.name}` : option.no
|
return option.name ? `${option.no} · ${option.name}` : option.no
|
||||||
@@ -85,9 +84,6 @@ function businessLabel(option: BusinessOption): string {
|
|||||||
|
|
||||||
// ==================== 新增 / 编辑 ====================
|
// ==================== 新增 / 编辑 ====================
|
||||||
|
|
||||||
/** 金额形态:非负数字、最多两位小数(与后端 `_check_amount` 同口径) */
|
|
||||||
const AMOUNT_PATTERN = /^(\d+)(\.\d{1,2})?$/
|
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const editingId = ref<number | null>(null)
|
const editingId = ref<number | null>(null)
|
||||||
/** 单一状态源:编辑中 ⇔ editingId 非空(避免双状态漂移) */
|
/** 单一状态源:编辑中 ⇔ editingId 非空(避免双状态漂移) */
|
||||||
@@ -124,9 +120,7 @@ const shipmentOptions = ref<ShipmentItem[]>([])
|
|||||||
const shipmentLoading = ref(false)
|
const shipmentLoading = ref(false)
|
||||||
|
|
||||||
/** 编辑中的回款(回显业务编号/批次语义用) */
|
/** 编辑中的回款(回显业务编号/批次语义用) */
|
||||||
const editingRow = computed(() =>
|
const editingRow = useEditingRow(records, editingId)
|
||||||
editingId.value === null ? null : records.value.find((r) => r.id === editingId.value) ?? null,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** 下拉展示:发货单号(发货日期,总额)—— 与任务约定文案一致 */
|
/** 下拉展示:发货单号(发货日期,总额)—— 与任务约定文案一致 */
|
||||||
function shipmentLabel(row: ShipmentItem): string {
|
function shipmentLabel(row: ShipmentItem): string {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import type { ShipmentDetail, ShipmentItem } from '@/api/shipments'
|
|||||||
import AppPagination from '@/components/table/AppPagination.vue'
|
import AppPagination from '@/components/table/AppPagination.vue'
|
||||||
import { useConfirmAction } from '@/composables/useConfirmAction'
|
import { useConfirmAction } from '@/composables/useConfirmAction'
|
||||||
import { usePagedList } from '@/composables/usePagedList'
|
import { usePagedList } from '@/composables/usePagedList'
|
||||||
|
import { AMOUNT_PATTERN, PRICE_PATTERN, ZERO_PATTERN } from '@/utils/validate'
|
||||||
|
import { useEditingRow } from '@/composables/useEditingRow'
|
||||||
|
import { useDropdownOptions } from '@/composables/useDropdownOptions'
|
||||||
import { handledError } from '@/utils/error'
|
import { handledError } from '@/utils/error'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,22 +58,18 @@ interface BusinessOption {
|
|||||||
status: number
|
status: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const businessOptions = ref<BusinessOption[]>([])
|
// 业务下拉数据源:一页 200 条覆盖标签页规模;超出规模由后端校验兜底
|
||||||
|
const { options: businessOptions, load: loadBusinessOptions } = useDropdownOptions<BusinessOption>(
|
||||||
async function loadBusinessOptions(): Promise<void> {
|
async () => {
|
||||||
try {
|
|
||||||
// 业务下拉数据源:一页 200 条覆盖标签页规模;超出规模由后端校验兜底
|
|
||||||
const data = await businessApi.listBusinesses({ pageNum: 1, pageSize: 200 })
|
const data = await businessApi.listBusinesses({ pageNum: 1, pageSize: 200 })
|
||||||
businessOptions.value = data.list.map((b) => ({
|
return data.list.map((b) => ({
|
||||||
id: b.id,
|
id: b.id,
|
||||||
no: b.business_no,
|
no: b.business_no,
|
||||||
name: b.customer_name ?? '',
|
name: b.customer_name ?? '',
|
||||||
status: b.status,
|
status: b.status,
|
||||||
}))
|
}))
|
||||||
} catch (error) {
|
},
|
||||||
handledError(error) // 下拉为空不阻断列表使用
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 下拉项展示:业务编号 + 客户名;终态业务加后缀标注(创建发货单会被后端拦) */
|
/** 下拉项展示:业务编号 + 客户名;终态业务加后缀标注(创建发货单会被后端拦) */
|
||||||
function businessLabel(option: BusinessOption): string {
|
function businessLabel(option: BusinessOption): string {
|
||||||
@@ -80,13 +79,6 @@ function businessLabel(option: BusinessOption): string {
|
|||||||
|
|
||||||
// ==================== 新增 / 编辑(含明细行编辑器) ====================
|
// ==================== 新增 / 编辑(含明细行编辑器) ====================
|
||||||
|
|
||||||
/** 金额/数量形态:非负数字、最多两位小数(与后端 `_check_amount` 同口径) */
|
|
||||||
const AMOUNT_PATTERN = /^(\d+)(\.\d{1,2})?$/
|
|
||||||
// 发货单价列建表为 Numeric(14,4):后端 str 直出带最多 4 位小数,前端放行到 4 位
|
|
||||||
const PRICE_PATTERN = /^(\d+)(\.\d{1,4})?$/
|
|
||||||
/** 全零形态:数量排除「>0」语义,不走 float 比较 */
|
|
||||||
const ZERO_PATTERN = /^0+(\.0+)?$/
|
|
||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const editingId = ref<number | null>(null)
|
const editingId = ref<number | null>(null)
|
||||||
/** 单一状态源:编辑中 ⇔ editingId 非空(避免双状态漂移) */
|
/** 单一状态源:编辑中 ⇔ editingId 非空(避免双状态漂移) */
|
||||||
@@ -95,9 +87,7 @@ const formRef = ref<FormInstance>()
|
|||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
|
||||||
/** 编辑中的发货单(回显业务编号用,编辑挂靠不可改) */
|
/** 编辑中的发货单(回显业务编号用,编辑挂靠不可改) */
|
||||||
const editingRow = computed(() =>
|
const editingRow = useEditingRow(records, editingId)
|
||||||
editingId.value === null ? null : records.value.find((r) => r.id === editingId.value) ?? null,
|
|
||||||
)
|
|
||||||
|
|
||||||
/** 明细行草稿(编辑时由详情回显;全量替换,行 id 不参与提交) */
|
/** 明细行草稿(编辑时由详情回显;全量替换,行 id 不参与提交) */
|
||||||
interface ItemDraft {
|
interface ItemDraft {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
} from '@/api/params'
|
} from '@/api/params'
|
||||||
import AppPagination from '@/components/table/AppPagination.vue'
|
import AppPagination from '@/components/table/AppPagination.vue'
|
||||||
import { useConfirmAction } from '@/composables/useConfirmAction'
|
import { useConfirmAction } from '@/composables/useConfirmAction'
|
||||||
|
import { usePagedList } from '@/composables/usePagedList'
|
||||||
import { handledError } from '@/utils/error'
|
import { handledError } from '@/utils/error'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,25 +190,20 @@ async function submitAll(): Promise<void> {
|
|||||||
// ==================== 版本管理抽屉 ====================
|
// ==================== 版本管理抽屉 ====================
|
||||||
|
|
||||||
const versionVisible = ref(false)
|
const versionVisible = ref(false)
|
||||||
const batches = ref<ParamBatch[]>([])
|
|
||||||
const batchesTotal = ref(0)
|
|
||||||
const batchesPage = ref(1)
|
|
||||||
const batchesPageSize = ref(10)
|
|
||||||
const batchesLoading = ref(false)
|
|
||||||
const detailCache = reactive<Record<number, ParamBatchDetail>>({})
|
const detailCache = reactive<Record<number, ParamBatchDetail>>({})
|
||||||
|
|
||||||
async function loadBatches(): Promise<void> {
|
// 分页五件套换 usePagedList:手写版没有请求序号防竞态(快速翻页时旧响应
|
||||||
batchesLoading.value = true
|
// 后到会覆盖新页数据)与页码回退——正是该 composable 注释里写明已修过的坑
|
||||||
try {
|
const {
|
||||||
const page = await paramsApi.listBatches({ pageNum: batchesPage.value, pageSize: batchesPageSize.value })
|
records: batches,
|
||||||
batches.value = page.list
|
total: batchesTotal,
|
||||||
batchesTotal.value = page.total
|
pageNum: batchesPage,
|
||||||
} catch (error) {
|
pageSize: batchesPageSize,
|
||||||
handledError(error)
|
loading: batchesLoading,
|
||||||
} finally {
|
load: loadBatches,
|
||||||
batchesLoading.value = false
|
onPageChange: onBatchesPageChange,
|
||||||
}
|
onSizeChange: onBatchesSizeChange,
|
||||||
}
|
} = usePagedList<ParamBatch, Record<string, never>>(paramsApi.listBatches, () => ({}))
|
||||||
|
|
||||||
async function detailOf(batch: ParamBatch): Promise<ParamBatchDetail> {
|
async function detailOf(batch: ParamBatch): Promise<ParamBatchDetail> {
|
||||||
if (detailCache[batch.id]) return detailCache[batch.id]
|
if (detailCache[batch.id]) return detailCache[batch.id]
|
||||||
@@ -299,11 +295,21 @@ function openVersionDrawer(): void {
|
|||||||
|
|
||||||
const historyVisible = ref(false)
|
const historyVisible = ref(false)
|
||||||
const historyItem = ref<ParamCatalogItem | null>(null)
|
const historyItem = ref<ParamCatalogItem | null>(null)
|
||||||
const historyRows = ref<ParamHistory[]>([])
|
|
||||||
const historyTotal = ref(0)
|
// 历史抽屉分页同样收口 usePagedList;fetcher 闭包当前参数的编码
|
||||||
const historyPage = ref(1)
|
const {
|
||||||
const historyPageSize = ref(10)
|
records: historyRows,
|
||||||
const historyLoading = ref(false)
|
total: historyTotal,
|
||||||
|
pageNum: historyPage,
|
||||||
|
pageSize: historyPageSize,
|
||||||
|
loading: historyLoading,
|
||||||
|
load: loadHistory,
|
||||||
|
onPageChange: onHistoryPageChange,
|
||||||
|
onSizeChange: onHistorySizeChange,
|
||||||
|
} = usePagedList<ParamHistory, Record<string, never>>(
|
||||||
|
(params) => paramsApi.getParamHistory(historyItem.value!.param_code, params),
|
||||||
|
() => ({}),
|
||||||
|
)
|
||||||
|
|
||||||
function openHistory(item: ParamCatalogItem): void {
|
function openHistory(item: ParamCatalogItem): void {
|
||||||
historyItem.value = item
|
historyItem.value = item
|
||||||
@@ -312,23 +318,6 @@ function openHistory(item: ParamCatalogItem): void {
|
|||||||
void loadHistory()
|
void loadHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadHistory(): Promise<void> {
|
|
||||||
if (!historyItem.value) return
|
|
||||||
historyLoading.value = true
|
|
||||||
try {
|
|
||||||
const page = await paramsApi.getParamHistory(historyItem.value.param_code, {
|
|
||||||
pageNum: historyPage.value,
|
|
||||||
pageSize: historyPageSize.value,
|
|
||||||
})
|
|
||||||
historyRows.value = page.list
|
|
||||||
historyTotal.value = page.total
|
|
||||||
} catch (error) {
|
|
||||||
handledError(error)
|
|
||||||
} finally {
|
|
||||||
historyLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 计算预览抽屉 ====================
|
// ==================== 计算预览抽屉 ====================
|
||||||
|
|
||||||
const previewVisible = ref(false)
|
const previewVisible = ref(false)
|
||||||
@@ -667,8 +656,8 @@ onMounted(() => {
|
|||||||
:total="batchesTotal"
|
:total="batchesTotal"
|
||||||
:page-num="batchesPage"
|
:page-num="batchesPage"
|
||||||
:page-size="batchesPageSize"
|
:page-size="batchesPageSize"
|
||||||
@page-change="(p: number) => { batchesPage = p; void loadBatches() }"
|
@page-change="onBatchesPageChange"
|
||||||
@size-change="(s: number) => { batchesPageSize = s; batchesPage = 1; void loadBatches() }"
|
@size-change="onBatchesSizeChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
@@ -692,8 +681,8 @@ onMounted(() => {
|
|||||||
:total="historyTotal"
|
:total="historyTotal"
|
||||||
:page-num="historyPage"
|
:page-num="historyPage"
|
||||||
:page-size="historyPageSize"
|
:page-size="historyPageSize"
|
||||||
@page-change="(p: number) => { historyPage = p; void loadHistory() }"
|
@page-change="onHistoryPageChange"
|
||||||
@size-change="(s: number) => { historyPageSize = s; historyPage = 1; void loadHistory() }"
|
@size-change="onHistorySizeChange"
|
||||||
/>
|
/>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user