feat: 完善协议缴费分期、样本编码搜索与鸿蒙附件选择
支持移动端样本编码远程关键字检索、分期起始日刷新与鸿蒙微信附件选取,并同步收款账户选择、角色页与相关设计文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user