feat: 完善协议缴费分期、样本编码搜索与鸿蒙附件选择

支持移动端样本编码远程关键字检索、分期起始日刷新与鸿蒙微信附件选取,并同步收款账户选择、角色页与相关设计文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-22 09:23:17 +08:00
1 parent 6e2656f999
commit 063727a7d8
45 files changed
+42669 -34713

No files matched your search

@@ -0,0 +1,23 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { calcStandardFqDueDate } from './protocol-payment-installment'
describe('calcStandardFqDueDate', () => {
it('builds six periods as start, +1 year, then +half year', () => {
const start = '20261021'
assert.deepEqual(
[0, 1, 2, 3, 4, 5].map((i) => calcStandardFqDueDate(start, i)),
['20261021', '20271021', '20280421', '20281021', '20290421', '20291021']
)
})
it('clamps month-end when adding half a year', () => {
assert.equal(calcStandardFqDueDate('20260831', 2), '20280229')
assert.equal(calcStandardFqDueDate('20260831', 1), '20270831')
})
it('returns empty for an invalid start date', () => {
assert.equal(calcStandardFqDueDate('', 0), '')
assert.equal(calcStandardFqDueDate('2026-10-21', -1), '')
})
})
@@ -215,6 +215,30 @@ export function calcFqInstallmentDueDate(
return base
}
/** 按日历加月,月末超出时落到该月最后一天 */
function addCalendarMonthsYmd(dateStr: string, months: number): string {
const compact = String(dateStr || '').replace(/-/g, '')
if (compact.length !== 8 || !Number.isFinite(months)) return ''
const y = Number(compact.slice(0, 4))
const monthIndex = Number(compact.slice(4, 6)) - 1 + Math.trunc(months)
const ny = y + Math.floor(monthIndex / 12)
const nm = monthIndex % 12
const lastDay = new Date(ny, nm + 1, 0).getDate()
const nd = Math.min(Number(compact.slice(6, 8)), lastDay)
return `${ny}${String(nm + 1).padStart(2, '0')}${String(nd).padStart(2, '0')}`
}
/**
* 分期默认应缴日(与每期间隔、提前天数无关):
* 第 1 期 = 开始日,第 2 期 = 第 1 期 + 1 年,第 3 期起每期在上一期基础上 + 半年。
*/
export function calcStandardFqDueDate(startDate: string, index: number): string {
const start = String(startDate || '').replace(/-/g, '')
if (start.length !== 8 || !Number.isInteger(index) || index < 0) return ''
if (index === 0) return start
return addCalendarMonthsYmd(start, 12 + (index - 1) * 6)
}
export function calcInstallmentDueAmounts(
contractAmount: number | null | undefined,
count: number
@@ -359,6 +359,7 @@
import {
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
calcFqInstallmentDueDate,
calcStandardFqDueDate,
normalizeInstallmentAmounts
} from '@/utils/protocol-payment-installment'
import {
@@ -607,7 +608,27 @@
return [...(scheduleByNo(pay, scheduleNo)?.transfers || [])]
}
function rebuildSchedules(pay: Record<string, any>): Record<string, any> {
function todayYmd(): string {
const now = new Date()
const pad = (n: number) => String(n).padStart(2, '0')
return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`
}
function computedScheduleDueDate(
mode: string,
startDate: string,
index: number,
interval: number,
advanceDays: number
): string {
if (mode === 'fq') return calcStandardFqDueDate(startDate, index)
return calcFqInstallmentDueDate(startDate, index, interval, advanceDays) || ''
}
function rebuildSchedules(
pay: Record<string, any>,
options?: { refreshDueDates?: boolean }
): Record<string, any> {
const mode = pay.payMode || 'dj'
if (mode === 'dj') {
const prev = scheduleByNo(pay, 1)
@@ -626,6 +647,7 @@
}
}
// fq / fgd / djfq:按期数生成批次;金额无模板时留空由用户手填
// 分期应缴日:第 1 期=开始日,第 2 期=+1 年,其后每期 +半年
const count = Number(pay.installmentCount) || 0
const interval = Number(pay.installmentIntervalMonths) || 0
const startDate = pay.installmentStartDate || ''
@@ -640,9 +662,12 @@
amounts = Array.from({ length: count }, () => null)
}
}
if (!count || !interval || !startDate) {
const canBuild =
mode === 'fq' ? Boolean(count && startDate) : Boolean(count && interval && startDate)
if (!canBuild) {
return { ...pay, installmentRatios: hasManualAmounts ? amounts : [], schedules: [] }
}
const refreshDueDates = options?.refreshDueDates === true
return {
...pay,
installmentRatios: hasManualAmounts ? amounts : [],
@@ -654,14 +679,19 @@
!hasManualAmounts && prevAmt != null && Number(prevAmt) > 0
? Number(prevAmt)
: null
const computedDue = computedScheduleDueDate(mode, startDate, i, interval, advanceDays)
const locked = prev ? isScheduleDueLocked(prev) : false
const dueDate =
locked && prev?.dueDate
? String(prev.dueDate)
: !refreshDueDates && prev?.dueDate
? String(prev.dueDate)
: computedDue
return {
id: prev?.id,
scheduleNo,
dueAmount: hasManualAmounts ? amounts[i] : keepPrev,
dueDate:
prev?.dueDate ||
calcFqInstallmentDueDate(startDate, i, interval, advanceDays) ||
'',
dueDate,
scheduleStatus: prev?.scheduleStatus || 'open',
transfers: transfersForSchedule(pay, scheduleNo)
}
@@ -675,10 +705,15 @@
emit('update:modelValue', [...(props.modelValue || [])])
}
function patchPay(pIdx: number, patch: Record<string, any>, rebuild = false) {
function patchPay(
pIdx: number,
patch: Record<string, any>,
rebuild = false,
rebuildOptions?: { refreshDueDates?: boolean }
) {
const list = [...(props.modelValue || [])]
let next = { ...list[pIdx], ...patch }
if (rebuild) next = rebuildSchedules(next)
if (rebuild) next = rebuildSchedules(next, rebuildOptions)
list[pIdx] = next
emit('update:modelValue', list)
}
@@ -794,11 +829,14 @@
patch.installmentIntervalMonths = 1
patch.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS
patch.installmentRatios = []
if (mode === 'fq' && !String(prev.installmentStartDate || '').trim()) {
patch.installmentStartDate = todayYmd()
}
} else {
patch.installmentCount = null
patch.installmentRatios = []
}
patchPay(pIdx, patch, true)
patchPay(pIdx, patch, true, { refreshDueDates: mode === 'fq' })
}
function onContractAmountChange(pIdx: number, v: number | null) {
@@ -817,7 +855,11 @@
}
function onFqStartDateChange(pIdx: number, v: string) {
patchPay(pIdx, { installmentStartDate: v || '' }, true)
const start = v || ''
const mode = String((props.modelValue || [])[pIdx]?.payMode || '')
patchPay(pIdx, { installmentStartDate: start }, true, {
refreshDueDates: mode === 'fq' && !!start
})
}
function addTransfer(pIdx: number, sIdx: number) {
+17 -2
View File
@@ -256,6 +256,7 @@
<DialogFormBody :loading="menuDialogLoading">
<!-- 父子节点独立勾选:父级可不选子级、子级可不选父级(如页面权限与按钮权限分开配置) -->
<el-tree
:key="menuTreeRenderKey"
ref="menuTreeRef"
:data="menuTreeData"
show-checkbox
@@ -541,6 +542,7 @@
const menuTreeRef = ref()
const menuTreeData = ref<Api.Menu.MenuItem[]>([])
const menuCheckedKeys = ref<string[]>([])
const menuTreeRenderKey = ref(0)
const menuEditingRoleId = ref<string>('')
// ---- 数据权限弹窗 ----
@@ -822,14 +824,27 @@
}
// ---- 菜单配置 ----
/**
* el-tree 的 default-checked-keys 只会增量勾选,不会取消已勾节点。
* 连续打开不同角色时,必须重挂载树并用 setCheckedKeys 整表替换勾选。
*/
async function syncMenuTreeChecks(menuIds: string[]) {
menuCheckedKeys.value = [...menuIds]
menuTreeRenderKey.value += 1
await nextTick()
menuTreeRef.value?.setCheckedKeys?.(menuCheckedKeys.value)
}
async function handleMenuConfig(row: Api.Role.RoleItem) {
menuEditingRoleId.value = row.id
menuDialogVisible.value = true
menuDialogLoading.value = true
const pending = Promise.all([fetchMenuTree(), fetchRoleMenuIds(row.id)])
try {
const [tree, res] = await Promise.all([fetchMenuTree(), fetchRoleMenuIds(row.id)])
await syncMenuTreeChecks([])
const [tree, res] = await pending
menuTreeData.value = tree || []
menuCheckedKeys.value = res?.menuIds || []
await syncMenuTreeChecks(res?.menuIds || [])
} finally {
menuDialogLoading.value = false
}