refactor(mobile-oa-h5): extract ScheduleTransferEditor for shared transfer UI
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
9de7f5456e
commit
c825c170e7
4 files changed
+668
-554
No files matched your search
@@ -231,140 +231,15 @@
|
||||
<b>¥{{ schedulePaid(sch).arrears }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<article
|
||||
v-for="(tr, tIdx) in sch.transfers || []"
|
||||
:key="tr.id || `${pIdx}-${sIdx}-${tIdx}`"
|
||||
class="protocol-sample-pay__transfer"
|
||||
>
|
||||
<header class="protocol-sample-pay__transfer-title">
|
||||
<strong>转账 {{ tIdx + 1 }}</strong>
|
||||
<van-tag class="market-tag" :type="payStatusTone(tr.payStatus)" round>
|
||||
{{ payStatusLabel(tr.payStatus) }}
|
||||
</van-tag>
|
||||
</header>
|
||||
<div class="market-form-fields">
|
||||
<div class="market-form-field">
|
||||
<label>转账方式</label>
|
||||
<div
|
||||
class="market-form-field__control"
|
||||
@click="!disabled && canEditTransfer(sch, tr) && openTransferTypeSheet(pIdx, sIdx, tIdx)"
|
||||
>
|
||||
<van-field
|
||||
:model-value="transferTypeLabel(tr.transferType)"
|
||||
is-link
|
||||
readonly
|
||||
placeholder="请选择"
|
||||
:disabled="disabled || !canEditTransfer(sch, tr)"
|
||||
:border="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="market-form-field">
|
||||
<label>转账金额</label>
|
||||
<van-field
|
||||
:model-value="tr.transferAmount == null ? '' : String(tr.transferAmount)"
|
||||
type="number"
|
||||
placeholder="请输入"
|
||||
:disabled="disabled || !canEditTransfer(sch, tr)"
|
||||
:border="false"
|
||||
@update:model-value="(v) => patchTransfer(pIdx, sIdx, tIdx, { transferAmount: parseAmount(v) })"
|
||||
/>
|
||||
</div>
|
||||
<div class="market-form-field">
|
||||
<label>转账时间</label>
|
||||
<div class="market-form-field__control">
|
||||
<OaDatePicker
|
||||
:model-value="tr.transferTime || ''"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:disabled="disabled || !canEditTransfer(sch, tr)"
|
||||
@update:model-value="(v) => patchTransfer(pIdx, sIdx, tIdx, { transferTime: v || '' })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="market-form-field">
|
||||
<label>转账凭证</label>
|
||||
<PresignAttachmentUploader
|
||||
:model-value="ensureTransferAttachments(tr)"
|
||||
:disabled="disabled || !canEditTransfer(sch, tr)"
|
||||
:uploading="isTransferUploading(pIdx, sIdx, tIdx)"
|
||||
@upload="(file) => uploadTransferFile(pIdx, sIdx, tIdx, file)"
|
||||
@remove="(index) => removeTransferFile(pIdx, sIdx, tIdx, index)"
|
||||
/>
|
||||
</div>
|
||||
<div class="market-form-field">
|
||||
<label>备注</label>
|
||||
<van-field
|
||||
:model-value="tr.remark || ''"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
maxlength="512"
|
||||
show-word-limit
|
||||
placeholder="选填"
|
||||
:disabled="disabled || !canEditTransfer(sch, tr)"
|
||||
:border="false"
|
||||
@update:model-value="(v) => patchTransfer(pIdx, sIdx, tIdx, { remark: v })"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="tr.financeRemark" class="market-form-field">
|
||||
<label>财务备注</label>
|
||||
<van-field
|
||||
:model-value="tr.financeRemark"
|
||||
readonly
|
||||
:border="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!disabled && hasTransferAction(sch, tr)" class="protocol-sample-pay__transfer-actions">
|
||||
<van-button
|
||||
v-if="canEditTransfer(sch, tr)"
|
||||
size="mini"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="busyKey === transferBusyKey(pIdx, sIdx, tIdx, tr)"
|
||||
@click="saveTransfer(pIdx, sIdx, tIdx)"
|
||||
>
|
||||
保存
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canSubmitTransfer(tr)"
|
||||
size="mini"
|
||||
type="primary"
|
||||
:loading="busyKey === `submit:${tr.id}`"
|
||||
@click="submitFinance(tr, pIdx, sIdx, tIdx)"
|
||||
>
|
||||
提交财务
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canWithdrawTransfer(tr)"
|
||||
size="mini"
|
||||
type="warning"
|
||||
:loading="busyKey === `withdraw:${tr.id}`"
|
||||
@click="withdrawFinance(tr, pIdx, sIdx, tIdx)"
|
||||
>
|
||||
撤回
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canDeleteTransfer(sch, tr)"
|
||||
size="mini"
|
||||
type="danger"
|
||||
plain
|
||||
@click="removeTransfer(pIdx, sIdx, tIdx)"
|
||||
>
|
||||
删除
|
||||
</van-button>
|
||||
</div>
|
||||
</article>
|
||||
<van-button
|
||||
v-if="!disabled && canAddTransfer(sch)"
|
||||
class="protocol-sample-pay__add-transfer"
|
||||
size="mini"
|
||||
type="primary"
|
||||
block
|
||||
@click="addTransfer(pIdx, sIdx)"
|
||||
>
|
||||
添加转账
|
||||
</van-button>
|
||||
<ScheduleTransferEditor
|
||||
:schedule="sch"
|
||||
:transfer-types="transferTypes"
|
||||
:market-role="marketRole"
|
||||
:context-id="protocolId"
|
||||
:disabled="disabled"
|
||||
@update:schedule="(next) => replaceSchedule(pIdx, sIdx, next)"
|
||||
@changed="onTransferChanged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -379,13 +254,6 @@
|
||||
close-on-click-action
|
||||
@select="onPayModeSelect"
|
||||
/>
|
||||
<van-action-sheet
|
||||
v-model:show="transferTypeSheetVisible"
|
||||
:actions="transferTypeSheetActions"
|
||||
cancel-text="取消"
|
||||
close-on-click-action
|
||||
@select="onTransferTypeSelect"
|
||||
/>
|
||||
<van-popup v-model:show="datePickerVisible" position="bottom" round>
|
||||
<van-date-picker
|
||||
v-model="datePickerValue"
|
||||
@@ -401,10 +269,8 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { showConfirmDialog, showSuccessToast, showToast } from 'vant'
|
||||
import OaDatePicker from '../../oa/components/OaDatePicker.vue'
|
||||
import { performMarketAction, uploadMarketAttachment } from '../../api/market.js'
|
||||
import PresignAttachmentUploader from './PresignAttachmentUploader.vue'
|
||||
import { showConfirmDialog, showToast } from 'vant'
|
||||
import ScheduleTransferEditor from './ScheduleTransferEditor.vue'
|
||||
import {
|
||||
applyConsolidateRemainingToPayment,
|
||||
applyExpandRemainingToInstallment,
|
||||
@@ -421,32 +287,11 @@ import {
|
||||
shouldAutoFillLastPeriodDue
|
||||
} from '../protocol-sample-payment.js'
|
||||
import {
|
||||
applySavedTransfer,
|
||||
assertCanSaveTransfer,
|
||||
buildTransferSavePayload,
|
||||
canSubmitFinance,
|
||||
canUseProtocolScheduleAction,
|
||||
canUseProtocolTransferAction,
|
||||
canWithdrawFinance,
|
||||
emptyTransferRow,
|
||||
ensureTransferAttachments,
|
||||
PAY_STATUS_LABELS,
|
||||
patchTransfersAt,
|
||||
validateTransferRow
|
||||
emptyTransferRow
|
||||
} from '../protocol-sample-transfer.js'
|
||||
import { marketErrorMessage } from '../market-error.js'
|
||||
import {
|
||||
createPendingMarketAttachment,
|
||||
dropPendingMarketAttachment,
|
||||
removeMarketAttachmentAt,
|
||||
resolvePendingMarketAttachment,
|
||||
withPendingMarketAttachment
|
||||
} from '../attachment-upload-state.js'
|
||||
import {
|
||||
applyTransferDeleteResult,
|
||||
clearTransferDirty,
|
||||
hasDirtyTransfers,
|
||||
isTransferDirty,
|
||||
markTransferDirty
|
||||
} from '../protocol-transfer-dirty.js'
|
||||
import { compareMoney, normalizeMoney } from '../money.js'
|
||||
@@ -472,16 +317,11 @@ const lastPeriodDueOverrideByKey = ref({})
|
||||
const modeSheetVisible = ref(false)
|
||||
const modeSheetPayIdx = ref(-1)
|
||||
const modeSheetActions = ref([])
|
||||
const transferTypeSheetVisible = ref(false)
|
||||
const transferTypeSheetActions = ref([])
|
||||
const transferTypeTarget = ref(null)
|
||||
const datePickerVisible = ref(false)
|
||||
const datePickerValue = ref(['2026', '10', '13'])
|
||||
const dateTarget = ref(null)
|
||||
const minDate = new Date(2020, 0, 1)
|
||||
const maxDate = new Date(2040, 11, 31)
|
||||
const busyKey = ref('')
|
||||
const uploadKeys = ref(new Set())
|
||||
|
||||
const syncedPayments = computed(() => props.modelValue || [])
|
||||
const expandedSchedules = ref(new Set())
|
||||
@@ -592,105 +432,23 @@ function isScheduleLocked(sch) {
|
||||
return (sch.transfers || []).some((t) => t.payStatus === 'yjf')
|
||||
}
|
||||
|
||||
function paymentContext() {
|
||||
return { module: 'payment', marketRole: props.marketRole || 'specialist' }
|
||||
}
|
||||
|
||||
function schedulePaid(sch) {
|
||||
return schedulePaidArrears(sch?.dueAmount, sch?.transfers || [])
|
||||
}
|
||||
|
||||
function payStatusLabel(status) {
|
||||
return PAY_STATUS_LABELS[String(status || 'dfj')] || '已付款'
|
||||
function replaceSchedule(pIdx, sIdx, nextSchedule) {
|
||||
const list = [...(props.modelValue || [])]
|
||||
const pay = { ...list[pIdx] }
|
||||
const schedules = [...(pay.schedules || [])]
|
||||
schedules[sIdx] = nextSchedule
|
||||
pay.schedules = schedules
|
||||
list[pIdx] = pay
|
||||
emitList(list)
|
||||
emit('transfer-dirty-change', hasDirtyTransfers(list))
|
||||
}
|
||||
|
||||
function payStatusTone(status) {
|
||||
const code = String(status || 'dfj')
|
||||
if (code === 'yjf') return 'success'
|
||||
if (code === 'jfqrz') return 'warning'
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
function transferTypeLabel(value) {
|
||||
const key = String(value || '').trim()
|
||||
const hit = (props.transferTypes || []).find((opt) => String(opt.value) === key)
|
||||
return hit?.label || key || '请选择'
|
||||
}
|
||||
|
||||
function parseAmount(raw) {
|
||||
if (raw === '' || raw == null) return null
|
||||
return String(raw)
|
||||
}
|
||||
|
||||
function transferBusyKey(pIdx, sIdx, tIdx, tr) {
|
||||
return `save:${tr?.id || `${pIdx}-${sIdx}-${tIdx}`}`
|
||||
}
|
||||
|
||||
function isTransferUploading(pIdx, sIdx, tIdx) {
|
||||
return uploadKeys.value.has(`${pIdx}-${sIdx}-${tIdx}`)
|
||||
}
|
||||
|
||||
function getTransferList(pIdx, sIdx) {
|
||||
return [...(props.modelValue?.[pIdx]?.schedules?.[sIdx]?.transfers || [])]
|
||||
}
|
||||
|
||||
function setTransferList(pIdx, sIdx, transfers) {
|
||||
const next = patchTransfersAt(props.modelValue, pIdx, sIdx, transfers)
|
||||
emitList(next)
|
||||
emit('transfer-dirty-change', hasDirtyTransfers(next))
|
||||
}
|
||||
|
||||
function patchTransfer(pIdx, sIdx, tIdx, patch) {
|
||||
const transfers = getTransferList(pIdx, sIdx)
|
||||
transfers[tIdx] = markTransferDirty({ ...transfers[tIdx], ...patch })
|
||||
setTransferList(pIdx, sIdx, transfers)
|
||||
}
|
||||
|
||||
function addTransfer(pIdx, sIdx) {
|
||||
const schedule = props.modelValue?.[pIdx]?.schedules?.[sIdx]
|
||||
if (!canAddTransfer(schedule)) return
|
||||
const transfers = getTransferList(pIdx, sIdx)
|
||||
transfers.push(markTransferDirty(emptyTransferRow()))
|
||||
setTransferList(pIdx, sIdx, transfers)
|
||||
const key = scheduleKey(pIdx, sIdx)
|
||||
if (!expandedSchedules.value.has(key)) {
|
||||
const next = new Set(expandedSchedules.value)
|
||||
next.add(key)
|
||||
expandedSchedules.value = next
|
||||
}
|
||||
}
|
||||
|
||||
async function removeTransfer(pIdx, sIdx, tIdx) {
|
||||
const schedule = props.modelValue?.[pIdx]?.schedules?.[sIdx]
|
||||
const transfer = schedule?.transfers?.[tIdx]
|
||||
if (!transfer || !canDeleteTransfer(schedule, transfer)) return
|
||||
try {
|
||||
await showConfirmDialog({ title: '删除转账', message: '确认删除该笔转账记录吗?' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!transfer.id) {
|
||||
const transfers = getTransferList(pIdx, sIdx)
|
||||
transfers.splice(tIdx, 1)
|
||||
setTransferList(pIdx, sIdx, transfers)
|
||||
return
|
||||
}
|
||||
busyKey.value = `delete:${transfer.id}`
|
||||
try {
|
||||
await performMarketAction(paymentContext(), 'delete-payment-transfer', {
|
||||
id: transfer.id
|
||||
})
|
||||
const next = applyTransferDeleteResult(
|
||||
{ samplePayments: props.modelValue }, pIdx, sIdx, tIdx, true
|
||||
).samplePayments
|
||||
emitList(next)
|
||||
emit('transfer-dirty-change', hasDirtyTransfers(next))
|
||||
showSuccessToast('转账记录已删除')
|
||||
} catch (error) {
|
||||
showToast(marketErrorMessage(error, '删除转账失败'))
|
||||
} finally {
|
||||
busyKey.value = ''
|
||||
}
|
||||
function onTransferChanged() {
|
||||
emit('transfer-dirty-change', hasDirtyTransfers(props.modelValue))
|
||||
}
|
||||
|
||||
function canAddTransfer(schedule) {
|
||||
@@ -701,197 +459,17 @@ function canAddTransfer(schedule) {
|
||||
return canUseProtocolScheduleAction(schedule, 'register-transfer')
|
||||
}
|
||||
|
||||
function canEditTransfer(schedule, transfer) {
|
||||
if (!transfer?.id) return canAddTransfer(schedule)
|
||||
return canUseProtocolTransferAction(transfer, 'edit-transfer')
|
||||
}
|
||||
|
||||
function canDeleteTransfer(schedule, transfer) {
|
||||
if (!transfer?.id) return canAddTransfer(schedule)
|
||||
return canUseProtocolTransferAction(transfer, 'delete-payment-transfer')
|
||||
}
|
||||
|
||||
function canSubmitTransfer(transfer) {
|
||||
return !isTransferDirty(transfer) &&
|
||||
canUseProtocolTransferAction(transfer, 'submit') &&
|
||||
canSubmitFinance(transfer, props.protocolId)
|
||||
}
|
||||
|
||||
function canWithdrawTransfer(transfer) {
|
||||
return canUseProtocolTransferAction(transfer, 'withdraw') &&
|
||||
canWithdrawFinance(transfer, props.protocolId)
|
||||
}
|
||||
|
||||
function hasTransferAction(schedule, transfer) {
|
||||
return canEditTransfer(schedule, transfer) ||
|
||||
canDeleteTransfer(schedule, transfer) ||
|
||||
canSubmitTransfer(transfer) ||
|
||||
canWithdrawTransfer(transfer)
|
||||
}
|
||||
|
||||
function openTransferTypeSheet(pIdx, sIdx, tIdx) {
|
||||
transferTypeTarget.value = { pIdx, sIdx, tIdx }
|
||||
transferTypeSheetActions.value = (props.transferTypes || []).map((opt) => ({
|
||||
name: opt.label || opt.value,
|
||||
value: opt.value
|
||||
}))
|
||||
if (!transferTypeSheetActions.value.length) {
|
||||
showToast('暂无转账方式选项')
|
||||
return
|
||||
function addTransfer(pIdx, sIdx) {
|
||||
const schedule = props.modelValue?.[pIdx]?.schedules?.[sIdx]
|
||||
if (!canAddTransfer(schedule)) return
|
||||
const transfers = [...(schedule.transfers || []), markTransferDirty(emptyTransferRow())]
|
||||
replaceSchedule(pIdx, sIdx, { ...schedule, transfers })
|
||||
const key = scheduleKey(pIdx, sIdx)
|
||||
if (!expandedSchedules.value.has(key)) {
|
||||
const next = new Set(expandedSchedules.value)
|
||||
next.add(key)
|
||||
expandedSchedules.value = next
|
||||
}
|
||||
transferTypeSheetVisible.value = true
|
||||
}
|
||||
|
||||
function onTransferTypeSelect(action) {
|
||||
const target = transferTypeTarget.value
|
||||
if (!target) return
|
||||
patchTransfer(target.pIdx, target.sIdx, target.tIdx, { transferType: action.value })
|
||||
}
|
||||
|
||||
async function saveTransfer(pIdx, sIdx, tIdx) {
|
||||
const sch = props.modelValue?.[pIdx]?.schedules?.[sIdx]
|
||||
const tr = sch?.transfers?.[tIdx]
|
||||
if (!tr || !canEditTransfer(sch, tr)) return
|
||||
try {
|
||||
assertCanSaveTransfer({ protocolId: props.protocolId, scheduleId: sch?.id })
|
||||
} catch (error) {
|
||||
showToast(error.message)
|
||||
return
|
||||
}
|
||||
const invalid = validateTransferRow(tr)
|
||||
if (invalid) {
|
||||
showToast(invalid)
|
||||
return
|
||||
}
|
||||
const key = transferBusyKey(pIdx, sIdx, tIdx, tr)
|
||||
busyKey.value = key
|
||||
try {
|
||||
const payload = buildTransferSavePayload(tr)
|
||||
let remote
|
||||
if (tr.id) {
|
||||
remote = await performMarketAction(paymentContext(), 'edit-transfer', {
|
||||
id: tr.id,
|
||||
data: payload
|
||||
})
|
||||
} else {
|
||||
remote = await performMarketAction(paymentContext(), 'register-transfer', {
|
||||
data: { scheduleId: String(sch.id), ...payload }
|
||||
})
|
||||
}
|
||||
const data = remote?.data || remote || {}
|
||||
const transfers = getTransferList(pIdx, sIdx)
|
||||
transfers[tIdx] = clearTransferDirty(applySavedTransfer(transfers[tIdx], data))
|
||||
setTransferList(pIdx, sIdx, transfers)
|
||||
showSuccessToast(tr.id ? '转账记录已保存' : '转账记录已创建')
|
||||
} catch (error) {
|
||||
showToast(marketErrorMessage(error, '保存转账失败'))
|
||||
} finally {
|
||||
busyKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function submitFinance(tr, pIdx, sIdx, tIdx) {
|
||||
if (!canSubmitTransfer(tr)) {
|
||||
showToast('请先保存该转账记录后再提交财务确认')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提交财务确认',
|
||||
message: '确认提交该笔转账给财务确认吗?'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
busyKey.value = `submit:${tr.id}`
|
||||
try {
|
||||
const remote = await performMarketAction(paymentContext(), 'submit', { id: tr.id })
|
||||
const data = remote?.data || remote || {}
|
||||
const transfers = getTransferList(pIdx, sIdx)
|
||||
transfers[tIdx] = applySavedTransfer(transfers[tIdx], { ...data, payStatus: data.payStatus || 'jfqrz' })
|
||||
setTransferList(pIdx, sIdx, transfers)
|
||||
showSuccessToast('已提交财务确认')
|
||||
} catch (error) {
|
||||
showToast(marketErrorMessage(error, '提交失败'))
|
||||
} finally {
|
||||
busyKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function withdrawFinance(tr, pIdx, sIdx, tIdx) {
|
||||
if (!canWithdrawTransfer(tr)) return
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '撤回',
|
||||
message: '确认撤回该笔转账的财务确认申请?撤回后可修改并重新提交。'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
busyKey.value = `withdraw:${tr.id}`
|
||||
try {
|
||||
const remote = await performMarketAction(paymentContext(), 'withdraw', { id: tr.id })
|
||||
const data = remote?.data || remote || {}
|
||||
const transfers = getTransferList(pIdx, sIdx)
|
||||
transfers[tIdx] = applySavedTransfer(transfers[tIdx], { ...data, payStatus: data.payStatus || 'dfj' })
|
||||
setTransferList(pIdx, sIdx, transfers)
|
||||
showSuccessToast('已撤回')
|
||||
} catch (error) {
|
||||
showToast(marketErrorMessage(error, '撤回失败'))
|
||||
} finally {
|
||||
busyKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadTransferFile(pIdx, sIdx, tIdx, file) {
|
||||
if (!file) return
|
||||
const key = `${pIdx}-${sIdx}-${tIdx}`
|
||||
const pending = createPendingMarketAttachment(file)
|
||||
const transfersBegin = getTransferList(pIdx, sIdx)
|
||||
const begin = { ...transfersBegin[tIdx] }
|
||||
ensureTransferAttachments(begin)
|
||||
begin.attachments = withPendingMarketAttachment(begin.attachments, pending)
|
||||
transfersBegin[tIdx] = markTransferDirty(begin)
|
||||
setTransferList(pIdx, sIdx, transfersBegin)
|
||||
uploadKeys.value = new Set([...uploadKeys.value, key])
|
||||
try {
|
||||
const uploaded = await uploadMarketAttachment(
|
||||
{ module: 'payment', marketRole: props.marketRole || 'specialist' },
|
||||
file
|
||||
)
|
||||
const transfers = getTransferList(pIdx, sIdx)
|
||||
const current = { ...transfers[tIdx] }
|
||||
ensureTransferAttachments(current)
|
||||
current.attachments = resolvePendingMarketAttachment(
|
||||
current.attachments,
|
||||
pending._pendingId,
|
||||
uploaded
|
||||
)
|
||||
transfers[tIdx] = markTransferDirty(current)
|
||||
setTransferList(pIdx, sIdx, transfers)
|
||||
showSuccessToast('附件上传成功')
|
||||
} catch (error) {
|
||||
const transfers = getTransferList(pIdx, sIdx)
|
||||
const current = { ...transfers[tIdx] }
|
||||
ensureTransferAttachments(current)
|
||||
current.attachments = dropPendingMarketAttachment(current.attachments, pending._pendingId)
|
||||
transfers[tIdx] = markTransferDirty(current)
|
||||
setTransferList(pIdx, sIdx, transfers)
|
||||
showToast(marketErrorMessage(error, '附件上传失败'))
|
||||
} finally {
|
||||
const next = new Set(uploadKeys.value)
|
||||
next.delete(key)
|
||||
uploadKeys.value = next
|
||||
}
|
||||
}
|
||||
|
||||
function removeTransferFile(pIdx, sIdx, tIdx, index) {
|
||||
const transfers = getTransferList(pIdx, sIdx)
|
||||
const current = { ...transfers[tIdx] }
|
||||
ensureTransferAttachments(current)
|
||||
current.attachments = removeMarketAttachmentAt(current.attachments, index)
|
||||
transfers[tIdx] = markTransferDirty(current)
|
||||
setTransferList(pIdx, sIdx, transfers)
|
||||
}
|
||||
|
||||
function activeScheduleIndices(schedules) {
|
||||
@@ -1421,50 +999,4 @@ function onDateConfirm({ selectedValues }) {
|
||||
.protocol-sample-pay__stat.is-arrears b {
|
||||
color: #b45309;
|
||||
}
|
||||
.protocol-sample-pay__transfer {
|
||||
margin: 8px 8px 10px;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e8f0ec;
|
||||
background: linear-gradient(180deg, #f8fffc, #fff);
|
||||
}
|
||||
.protocol-sample-pay__transfer-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.protocol-sample-pay__attach,
|
||||
.protocol-sample-pay__datetime {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
.protocol-sample-pay__attach label,
|
||||
.protocol-sample-pay__datetime label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: #3d524a;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
}
|
||||
.protocol-sample-pay__transfer-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
.protocol-sample-pay__transfer-actions :deep(.van-button),
|
||||
.protocol-sample-pay__add-transfer {
|
||||
min-width: auto;
|
||||
min-height: 28px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.protocol-sample-pay__add-transfer {
|
||||
margin: 0 8px;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,527 @@
|
||||
<template>
|
||||
<div class="protocol-sample-pay__transfers">
|
||||
<article
|
||||
v-for="(tr, tIdx) in transfers"
|
||||
:key="tr.id || tIdx"
|
||||
class="protocol-sample-pay__transfer"
|
||||
>
|
||||
<header class="protocol-sample-pay__transfer-title">
|
||||
<strong>转账 {{ tIdx + 1 }}</strong>
|
||||
<van-tag class="market-tag" :type="payStatusTone(tr.payStatus)" round>
|
||||
{{ payStatusLabel(tr.payStatus) }}
|
||||
</van-tag>
|
||||
</header>
|
||||
<div class="market-form-fields">
|
||||
<div class="market-form-field">
|
||||
<label>转账方式</label>
|
||||
<div
|
||||
class="market-form-field__control"
|
||||
@click="!disabled && canEditTransfer(tr) && openTransferTypeSheet(tIdx)"
|
||||
>
|
||||
<van-field
|
||||
:model-value="transferTypeLabel(tr.transferType)"
|
||||
is-link
|
||||
readonly
|
||||
placeholder="请选择"
|
||||
:disabled="disabled || !canEditTransfer(tr)"
|
||||
:border="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="market-form-field">
|
||||
<label>转账金额</label>
|
||||
<van-field
|
||||
:model-value="tr.transferAmount == null ? '' : String(tr.transferAmount)"
|
||||
type="number"
|
||||
placeholder="请输入"
|
||||
:disabled="disabled || !canEditTransfer(tr)"
|
||||
:border="false"
|
||||
@update:model-value="(v) => patchTransfer(tIdx, { transferAmount: parseAmount(v) })"
|
||||
/>
|
||||
</div>
|
||||
<div class="market-form-field">
|
||||
<label>转账时间</label>
|
||||
<div class="market-form-field__control">
|
||||
<OaDatePicker
|
||||
:model-value="tr.transferTime || ''"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
:disabled="disabled || !canEditTransfer(tr)"
|
||||
@update:model-value="(v) => patchTransfer(tIdx, { transferTime: v || '' })"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="market-form-field">
|
||||
<label>转账凭证</label>
|
||||
<PresignAttachmentUploader
|
||||
:model-value="ensureTransferAttachments(tr)"
|
||||
:disabled="disabled || !canEditTransfer(tr)"
|
||||
:uploading="isTransferUploading(tIdx)"
|
||||
@upload="(file) => uploadTransferFile(tIdx, file)"
|
||||
@remove="(index) => removeTransferFile(tIdx, index)"
|
||||
/>
|
||||
</div>
|
||||
<div class="market-form-field">
|
||||
<label>备注</label>
|
||||
<van-field
|
||||
:model-value="tr.remark || ''"
|
||||
type="textarea"
|
||||
rows="2"
|
||||
maxlength="512"
|
||||
show-word-limit
|
||||
placeholder="选填"
|
||||
:disabled="disabled || !canEditTransfer(tr)"
|
||||
:border="false"
|
||||
@update:model-value="(v) => patchTransfer(tIdx, { remark: v })"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="tr.financeRemark" class="market-form-field">
|
||||
<label>财务备注</label>
|
||||
<van-field
|
||||
:model-value="tr.financeRemark"
|
||||
readonly
|
||||
:border="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!disabled && hasTransferAction(tr)" class="protocol-sample-pay__transfer-actions">
|
||||
<van-button
|
||||
v-if="canEditTransfer(tr)"
|
||||
size="mini"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="busyKey === transferBusyKey(tIdx, tr)"
|
||||
@click="saveTransfer(tIdx)"
|
||||
>
|
||||
保存
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canSubmitTransfer(tr)"
|
||||
size="mini"
|
||||
type="primary"
|
||||
:loading="busyKey === `submit:${tr.id}`"
|
||||
@click="submitFinance(tr, tIdx)"
|
||||
>
|
||||
提交财务
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canWithdrawTransfer(tr)"
|
||||
size="mini"
|
||||
type="warning"
|
||||
:loading="busyKey === `withdraw:${tr.id}`"
|
||||
@click="withdrawFinance(tr, tIdx)"
|
||||
>
|
||||
撤回
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canDeleteTransfer(tr)"
|
||||
size="mini"
|
||||
type="danger"
|
||||
plain
|
||||
@click="removeTransfer(tIdx)"
|
||||
>
|
||||
删除
|
||||
</van-button>
|
||||
</div>
|
||||
</article>
|
||||
<van-button
|
||||
v-if="!disabled && canAddTransfer"
|
||||
class="protocol-sample-pay__add-transfer"
|
||||
size="mini"
|
||||
type="primary"
|
||||
block
|
||||
@click="addTransfer"
|
||||
>
|
||||
添加转账
|
||||
</van-button>
|
||||
|
||||
<van-action-sheet
|
||||
v-model:show="transferTypeSheetVisible"
|
||||
:actions="transferTypeSheetActions"
|
||||
cancel-text="取消"
|
||||
close-on-click-action
|
||||
@select="onTransferTypeSelect"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { showConfirmDialog, showSuccessToast, showToast } from 'vant'
|
||||
import OaDatePicker from '../../oa/components/OaDatePicker.vue'
|
||||
import { performMarketAction, uploadMarketAttachment } from '../../api/market.js'
|
||||
import PresignAttachmentUploader from './PresignAttachmentUploader.vue'
|
||||
import {
|
||||
applySavedTransfer,
|
||||
assertCanSaveTransfer,
|
||||
buildTransferSavePayload,
|
||||
canSubmitFinance,
|
||||
canUseProtocolScheduleAction,
|
||||
canUseProtocolTransferAction,
|
||||
canWithdrawFinance,
|
||||
emptyTransferRow,
|
||||
ensureTransferAttachments,
|
||||
PAY_STATUS_LABELS,
|
||||
validateTransferRow
|
||||
} from '../protocol-sample-transfer.js'
|
||||
import { marketErrorMessage } from '../market-error.js'
|
||||
import {
|
||||
createPendingMarketAttachment,
|
||||
dropPendingMarketAttachment,
|
||||
removeMarketAttachmentAt,
|
||||
resolvePendingMarketAttachment,
|
||||
withPendingMarketAttachment
|
||||
} from '../attachment-upload-state.js'
|
||||
import {
|
||||
applyTransferDeleteResult,
|
||||
clearTransferDirty,
|
||||
isTransferDirty,
|
||||
markTransferDirty
|
||||
} from '../protocol-transfer-dirty.js'
|
||||
|
||||
const props = defineProps({
|
||||
schedule: { type: Object, required: true },
|
||||
transferTypes: { type: Array, default: () => [] },
|
||||
marketRole: { type: String, required: true },
|
||||
contextId: { type: String, required: true },
|
||||
disabled: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:schedule', 'changed'])
|
||||
|
||||
const busyKey = ref('')
|
||||
const uploadKeys = ref(new Set())
|
||||
const transferTypeSheetVisible = ref(false)
|
||||
const transferTypeSheetActions = ref([])
|
||||
const transferTypeTarget = ref(-1)
|
||||
|
||||
const transfers = computed(() => props.schedule?.transfers || [])
|
||||
|
||||
const canAddTransfer = computed(() => {
|
||||
const schedule = props.schedule
|
||||
if (!schedule) return false
|
||||
const actions = schedule.allowedActions
|
||||
if (!Array.isArray(actions) || actions.length === 0) return true
|
||||
return canUseProtocolScheduleAction(schedule, 'register-transfer')
|
||||
})
|
||||
|
||||
function paymentContext() {
|
||||
return { module: 'payment', marketRole: props.marketRole || 'specialist' }
|
||||
}
|
||||
|
||||
function emitSchedule(nextSchedule, { changed = false } = {}) {
|
||||
emit('update:schedule', nextSchedule)
|
||||
if (changed) emit('changed')
|
||||
}
|
||||
|
||||
function withTransfers(nextTransfers) {
|
||||
return {
|
||||
...props.schedule,
|
||||
transfers: nextTransfers
|
||||
}
|
||||
}
|
||||
|
||||
function payStatusLabel(status) {
|
||||
return PAY_STATUS_LABELS[String(status || 'dfj')] || '已付款'
|
||||
}
|
||||
|
||||
function payStatusTone(status) {
|
||||
const code = String(status || 'dfj')
|
||||
if (code === 'yjf') return 'success'
|
||||
if (code === 'jfqrz') return 'warning'
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
function transferTypeLabel(value) {
|
||||
const key = String(value || '').trim()
|
||||
const hit = (props.transferTypes || []).find((opt) => String(opt.value) === key)
|
||||
return hit?.label || key || '请选择'
|
||||
}
|
||||
|
||||
function parseAmount(raw) {
|
||||
if (raw === '' || raw == null) return null
|
||||
return String(raw)
|
||||
}
|
||||
|
||||
function transferBusyKey(tIdx, tr) {
|
||||
return `save:${tr?.id || tIdx}`
|
||||
}
|
||||
|
||||
function isTransferUploading(tIdx) {
|
||||
return uploadKeys.value.has(String(tIdx))
|
||||
}
|
||||
|
||||
function canEditTransfer(transfer) {
|
||||
if (!transfer?.id) return canAddTransfer.value
|
||||
return canUseProtocolTransferAction(transfer, 'edit-transfer')
|
||||
}
|
||||
|
||||
function canDeleteTransfer(transfer) {
|
||||
if (!transfer?.id) return canAddTransfer.value
|
||||
return canUseProtocolTransferAction(transfer, 'delete-payment-transfer')
|
||||
}
|
||||
|
||||
function canSubmitTransfer(transfer) {
|
||||
return !isTransferDirty(transfer) &&
|
||||
canUseProtocolTransferAction(transfer, 'submit') &&
|
||||
canSubmitFinance(transfer, props.contextId)
|
||||
}
|
||||
|
||||
function canWithdrawTransfer(transfer) {
|
||||
return canUseProtocolTransferAction(transfer, 'withdraw') &&
|
||||
canWithdrawFinance(transfer, props.contextId)
|
||||
}
|
||||
|
||||
function hasTransferAction(transfer) {
|
||||
return canEditTransfer(transfer) ||
|
||||
canDeleteTransfer(transfer) ||
|
||||
canSubmitTransfer(transfer) ||
|
||||
canWithdrawTransfer(transfer)
|
||||
}
|
||||
|
||||
function patchTransfer(tIdx, patch) {
|
||||
const next = [...transfers.value]
|
||||
next[tIdx] = markTransferDirty({ ...next[tIdx], ...patch })
|
||||
emitSchedule(withTransfers(next))
|
||||
}
|
||||
|
||||
function addTransfer() {
|
||||
if (!canAddTransfer.value) return
|
||||
const next = [...transfers.value, markTransferDirty(emptyTransferRow())]
|
||||
emitSchedule(withTransfers(next))
|
||||
}
|
||||
|
||||
async function removeTransfer(tIdx) {
|
||||
const transfer = transfers.value[tIdx]
|
||||
if (!transfer || !canDeleteTransfer(transfer)) return
|
||||
try {
|
||||
await showConfirmDialog({ title: '删除转账', message: '确认删除该笔转账记录吗?' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!transfer.id) {
|
||||
const next = [...transfers.value]
|
||||
next.splice(tIdx, 1)
|
||||
emitSchedule(withTransfers(next), { changed: true })
|
||||
return
|
||||
}
|
||||
busyKey.value = `delete:${transfer.id}`
|
||||
try {
|
||||
await performMarketAction(paymentContext(), 'delete-payment-transfer', {
|
||||
id: transfer.id
|
||||
})
|
||||
const patched = applyTransferDeleteResult(
|
||||
{ samplePayments: [{ schedules: [props.schedule] }] },
|
||||
0,
|
||||
0,
|
||||
tIdx,
|
||||
true
|
||||
).samplePayments[0].schedules[0]
|
||||
emitSchedule(patched, { changed: true })
|
||||
showSuccessToast('转账记录已删除')
|
||||
} catch (error) {
|
||||
showToast(marketErrorMessage(error, '删除转账失败'))
|
||||
} finally {
|
||||
busyKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function openTransferTypeSheet(tIdx) {
|
||||
transferTypeTarget.value = tIdx
|
||||
transferTypeSheetActions.value = (props.transferTypes || []).map((opt) => ({
|
||||
name: opt.label || opt.value,
|
||||
value: opt.value
|
||||
}))
|
||||
if (!transferTypeSheetActions.value.length) {
|
||||
showToast('暂无转账方式选项')
|
||||
return
|
||||
}
|
||||
transferTypeSheetVisible.value = true
|
||||
}
|
||||
|
||||
function onTransferTypeSelect(action) {
|
||||
if (transferTypeTarget.value < 0) return
|
||||
patchTransfer(transferTypeTarget.value, { transferType: action.value })
|
||||
}
|
||||
|
||||
async function saveTransfer(tIdx) {
|
||||
const tr = transfers.value[tIdx]
|
||||
if (!tr || !canEditTransfer(tr)) return
|
||||
try {
|
||||
assertCanSaveTransfer({ contextId: props.contextId, scheduleId: props.schedule?.id })
|
||||
} catch (error) {
|
||||
showToast(error.message)
|
||||
return
|
||||
}
|
||||
const invalid = validateTransferRow(tr)
|
||||
if (invalid) {
|
||||
showToast(invalid)
|
||||
return
|
||||
}
|
||||
busyKey.value = transferBusyKey(tIdx, tr)
|
||||
try {
|
||||
const payload = buildTransferSavePayload(tr)
|
||||
let remote
|
||||
if (tr.id) {
|
||||
remote = await performMarketAction(paymentContext(), 'edit-transfer', {
|
||||
id: tr.id,
|
||||
data: payload
|
||||
})
|
||||
} else {
|
||||
remote = await performMarketAction(paymentContext(), 'register-transfer', {
|
||||
data: { scheduleId: String(props.schedule.id), ...payload }
|
||||
})
|
||||
}
|
||||
const data = remote?.data || remote || {}
|
||||
const next = [...transfers.value]
|
||||
next[tIdx] = clearTransferDirty(applySavedTransfer(next[tIdx], data))
|
||||
emitSchedule(withTransfers(next), { changed: true })
|
||||
showSuccessToast(tr.id ? '转账记录已保存' : '转账记录已创建')
|
||||
} catch (error) {
|
||||
showToast(marketErrorMessage(error, '保存转账失败'))
|
||||
} finally {
|
||||
busyKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function submitFinance(tr, tIdx) {
|
||||
if (!canSubmitTransfer(tr)) {
|
||||
showToast('请先保存该转账记录后再提交财务确认')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提交财务确认',
|
||||
message: '确认提交该笔转账给财务确认吗?'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
busyKey.value = `submit:${tr.id}`
|
||||
try {
|
||||
const remote = await performMarketAction(paymentContext(), 'submit', { id: tr.id })
|
||||
const data = remote?.data || remote || {}
|
||||
const next = [...transfers.value]
|
||||
next[tIdx] = applySavedTransfer(next[tIdx], { ...data, payStatus: data.payStatus || 'jfqrz' })
|
||||
emitSchedule(withTransfers(next), { changed: true })
|
||||
showSuccessToast('已提交财务确认')
|
||||
} catch (error) {
|
||||
showToast(marketErrorMessage(error, '提交失败'))
|
||||
} finally {
|
||||
busyKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function withdrawFinance(tr, tIdx) {
|
||||
if (!canWithdrawTransfer(tr)) return
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '撤回',
|
||||
message: '确认撤回该笔转账的财务确认申请?撤回后可修改并重新提交。'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
busyKey.value = `withdraw:${tr.id}`
|
||||
try {
|
||||
const remote = await performMarketAction(paymentContext(), 'withdraw', { id: tr.id })
|
||||
const data = remote?.data || remote || {}
|
||||
const next = [...transfers.value]
|
||||
next[tIdx] = applySavedTransfer(next[tIdx], { ...data, payStatus: data.payStatus || 'dfj' })
|
||||
emitSchedule(withTransfers(next), { changed: true })
|
||||
showSuccessToast('已撤回')
|
||||
} catch (error) {
|
||||
showToast(marketErrorMessage(error, '撤回失败'))
|
||||
} finally {
|
||||
busyKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadTransferFile(tIdx, file) {
|
||||
if (!file) return
|
||||
const key = String(tIdx)
|
||||
const pending = createPendingMarketAttachment(file)
|
||||
const beginList = [...transfers.value]
|
||||
const begin = { ...beginList[tIdx] }
|
||||
ensureTransferAttachments(begin)
|
||||
begin.attachments = withPendingMarketAttachment(begin.attachments, pending)
|
||||
beginList[tIdx] = markTransferDirty(begin)
|
||||
emitSchedule(withTransfers(beginList))
|
||||
uploadKeys.value = new Set([...uploadKeys.value, key])
|
||||
try {
|
||||
const uploaded = await uploadMarketAttachment(
|
||||
{ module: 'payment', marketRole: props.marketRole || 'specialist' },
|
||||
file
|
||||
)
|
||||
const list = [...(props.schedule?.transfers || [])]
|
||||
const current = { ...list[tIdx] }
|
||||
ensureTransferAttachments(current)
|
||||
current.attachments = resolvePendingMarketAttachment(
|
||||
current.attachments,
|
||||
pending._pendingId,
|
||||
uploaded
|
||||
)
|
||||
list[tIdx] = markTransferDirty(current)
|
||||
emitSchedule(withTransfers(list))
|
||||
showSuccessToast('附件上传成功')
|
||||
} catch (error) {
|
||||
const list = [...(props.schedule?.transfers || [])]
|
||||
const current = { ...list[tIdx] }
|
||||
ensureTransferAttachments(current)
|
||||
current.attachments = dropPendingMarketAttachment(current.attachments, pending._pendingId)
|
||||
list[tIdx] = markTransferDirty(current)
|
||||
emitSchedule(withTransfers(list))
|
||||
showToast(marketErrorMessage(error, '附件上传失败'))
|
||||
} finally {
|
||||
const next = new Set(uploadKeys.value)
|
||||
next.delete(key)
|
||||
uploadKeys.value = next
|
||||
}
|
||||
}
|
||||
|
||||
function removeTransferFile(tIdx, index) {
|
||||
const list = [...transfers.value]
|
||||
const current = { ...list[tIdx] }
|
||||
ensureTransferAttachments(current)
|
||||
current.attachments = removeMarketAttachmentAt(current.attachments, index)
|
||||
list[tIdx] = markTransferDirty(current)
|
||||
emitSchedule(withTransfers(list))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.protocol-sample-pay__transfer {
|
||||
margin: 8px 8px 10px;
|
||||
padding: 10px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e8f0ec;
|
||||
background: linear-gradient(180deg, #f8fffc, #fff);
|
||||
}
|
||||
.protocol-sample-pay__transfer-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.protocol-sample-pay__transfer-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: flex-end;
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
.protocol-sample-pay__transfer-actions :deep(.van-button),
|
||||
.protocol-sample-pay__add-transfer {
|
||||
min-width: auto;
|
||||
min-height: 28px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.protocol-sample-pay__add-transfer {
|
||||
margin: 0 8px;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
</style>
|
||||
@@ -82,6 +82,7 @@ test('market list visual contract keeps search white, tags filled and actions ri
|
||||
'./components/PresignOrderCard.vue',
|
||||
'./components/PresignTransferCard.vue',
|
||||
'./components/ProtocolSamplePaymentPanel.vue',
|
||||
'./components/ScheduleTransferEditor.vue',
|
||||
'./components/ProtocolSampleTransportPanel.vue',
|
||||
'./components/ProtocolV1PaymentPanel.vue',
|
||||
'../views/market/MarketModuleDetail.vue'
|
||||
@@ -449,13 +450,15 @@ test('market forms share attachment cards and preserve their existing value shap
|
||||
assert.match(genericForm, /fieldsLocked/)
|
||||
|
||||
const protocolPay = source('./components/ProtocolSamplePaymentPanel.vue')
|
||||
const transferEditor = source('./components/ScheduleTransferEditor.vue')
|
||||
const protocolV1Pay = source('./components/ProtocolV1PaymentPanel.vue')
|
||||
const presignForm = source('../views/market/presign/PresignOrderForm.vue')
|
||||
for (const content of [protocolPay, protocolV1Pay, presignForm, genericForm]) {
|
||||
assert.match(protocolPay, /<ScheduleTransferEditor/)
|
||||
for (const content of [transferEditor, protocolV1Pay, presignForm, genericForm]) {
|
||||
assert.match(content, /attachment-upload-state/)
|
||||
}
|
||||
assert.match(protocolPay, /createPendingMarketAttachment/)
|
||||
assert.match(protocolPay, /withPendingMarketAttachment/)
|
||||
assert.match(transferEditor, /createPendingMarketAttachment/)
|
||||
assert.match(transferEditor, /withPendingMarketAttachment/)
|
||||
assert.match(protocolV1Pay, /removeMarketAttachmentAt/)
|
||||
assert.match(presignForm, /createPendingMarketAttachment/)
|
||||
})
|
||||
@@ -498,7 +501,7 @@ test('payment detail presents every web field and nested transfer field with Chi
|
||||
assert.deepEqual(sections.map((section) => section.title), ['缴费概览', '应缴批次'])
|
||||
assert.deepEqual(
|
||||
sections[0].fields.map((field) => field.label),
|
||||
['协议号', '客户', '联系电话', '样本编码', '缴费方式', '合同额', '已缴 / 待缴', '整体进度', '协议状态', '缴费进度']
|
||||
['客户', '联系电话', '样本编码', '缴费方式', '合同额', '已缴 / 待缴', '整体进度', '协议状态', '缴费进度']
|
||||
)
|
||||
assert.equal(sections[1].cards[0].variant, 'schedule')
|
||||
assert.deepEqual(
|
||||
@@ -611,8 +614,6 @@ test('protocol, freight and presign details define explicit Chinese nested cards
|
||||
customer: { motherName: '张三' },
|
||||
samples: [{ id: 's1', sampleCode: 'S-1', sampleTypeName: '脐带血' }],
|
||||
attachments: { protocolAttachments: 'a.pdf' },
|
||||
storageCert: { makeStatus: '0', certImageUrls: 'cert.jpg' },
|
||||
fullmoonTodo: { handled: false, insuranceAttachments: 'ins.pdf', handleTime: '2026-10-01' },
|
||||
audits: []
|
||||
})
|
||||
const freight = getMarketModule('freight').detailSections({
|
||||
@@ -634,13 +635,8 @@ test('protocol, freight and presign details define explicit Chinese nested cards
|
||||
}
|
||||
}
|
||||
assert.ok(protocol.find((section) => section.key === 'samples').cards.length)
|
||||
const storageCert = protocol.find((section) => section.key === 'storageCert')
|
||||
const insurance = protocol.find((section) => section.key === 'insurance')
|
||||
assert.ok(storageCert)
|
||||
assert.ok(insurance)
|
||||
assert.ok(storageCert.fields.some((field) => field.type === 'attachments' && field.label === '储存证图片'))
|
||||
assert.ok(insurance.fields.some((field) => field.type === 'attachments' && field.label === '保险附件'))
|
||||
assert.ok(insurance.fields.some((field) => field.label === '办理时间'))
|
||||
assert.equal(protocol.find((section) => section.key === 'storageCert'), undefined)
|
||||
assert.equal(protocol.find((section) => section.key === 'insurance'), undefined)
|
||||
assert.ok(protocol.find((section) => section.key === 'audits'))
|
||||
assert.ok(protocol.find((section) => section.key === 'statusChange'))
|
||||
assert.ok(freight.find((section) => section.key === 'items').cards.length)
|
||||
@@ -730,21 +726,22 @@ test('protocol create and edit forms expose every writable web business field',
|
||||
assert.match(samplePanel, /searchable/)
|
||||
assert.match(samplePanel, /market-form-field/)
|
||||
const payPanel = source('../market/components/ProtocolSamplePaymentPanel.vue')
|
||||
const transferEditor = source('../market/components/ScheduleTransferEditor.vue')
|
||||
assert.match(payPanel, /applyPreferentialPriceToPayment/)
|
||||
assert.match(payPanel, /lastPreferentialByCode/)
|
||||
assert.match(payPanel, /<ScheduleTransferEditor/)
|
||||
assert.match(payPanel, /添加转账/)
|
||||
assert.match(payPanel, /提交财务确认/)
|
||||
assert.match(payPanel, /撤回/)
|
||||
assert.match(transferEditor, /提交财务确认/)
|
||||
assert.match(transferEditor, /撤回/)
|
||||
assert.match(payPanel, /protocolId/)
|
||||
assert.match(payPanel, /canUseProtocolScheduleAction/)
|
||||
assert.match(payPanel, /canUseProtocolTransferAction/)
|
||||
assert.match(transferEditor, /canUseProtocolTransferAction/)
|
||||
assert.doesNotMatch(payPanel, /canManageTransfers/)
|
||||
assert.match(payPanel, /performMarketAction|register-transfer/)
|
||||
assert.match(payPanel, /module:\s*'payment'/)
|
||||
assert.match(transferEditor, /performMarketAction|register-transfer/)
|
||||
assert.match(transferEditor, /module:\s*'payment'/)
|
||||
assert.match(payPanel, /协议总金额/)
|
||||
assert.match(payPanel, /protocolTotalAmount/)
|
||||
assert.match(payPanel, /market-form-field/)
|
||||
assert.match(payPanel, /添加转账/)
|
||||
assert.match(payPanel, /canAddTransfer/)
|
||||
assert.match(samplePanel, /loadExistingFreights/)
|
||||
assert.match(samplePanel, /includeItems/)
|
||||
@@ -756,14 +753,13 @@ test('protocol create and edit forms expose every writable web business field',
|
||||
assert.match(form, /currentUserRegions|applyCollectionRegion/)
|
||||
assert.match(form, /onProtocolTotalAmountChange/)
|
||||
assert.doesNotMatch(form, /样本合同合计/)
|
||||
assert.doesNotMatch(payPanel, /module:\s*'protocol'[\s\S]*uploadTransferFile/)
|
||||
assert.doesNotMatch(transferEditor, /module:\s*'protocol'[\s\S]*uploadTransferFile/)
|
||||
assert.match(form, /:protocol-id=/)
|
||||
assert.doesNotMatch(form, /:can-manage-transfers="true"/)
|
||||
assert.match(form, /presignDepositHint/)
|
||||
assert.match(form, /depositTransfers/)
|
||||
const protocolModule = source('../market/modules/protocol.js')
|
||||
assert.match(protocolModule, /意向金(关联预签约)/)
|
||||
assert.match(protocolModule, /储存证/)
|
||||
assert.match(protocolModule, /审核记录/)
|
||||
assert.match(protocolModule, /状态变更记录/)
|
||||
assert.match(protocolModule, /运输方式/)
|
||||
@@ -792,3 +788,11 @@ test('protocol detail filters actions when from=payment', () => {
|
||||
|
||||
assert.match(detail, /isPaymentEntryView|filterProtocolActionsForPaymentEntry/)
|
||||
})
|
||||
|
||||
test('ScheduleTransferEditor exposes save submit withdraw and MarketAttachmentCards', () => {
|
||||
const src = source('./components/ScheduleTransferEditor.vue')
|
||||
assert.match(src, /MarketAttachmentCards|PresignAttachmentUploader/)
|
||||
assert.match(src, /提交财务/)
|
||||
assert.match(src, /撤回/)
|
||||
assert.match(src, /添加转账/)
|
||||
})
|
||||
@@ -20,7 +20,7 @@ function cardLabels(card) {
|
||||
]
|
||||
}
|
||||
|
||||
test('protocol detail exposes reception qc production storage and release information', () => {
|
||||
test('protocol detail exposes reception and storage release without qc production or cert sections', () => {
|
||||
const sections = getMarketModule('protocol').detailSections({
|
||||
source: 'cell_storage',
|
||||
basic: { protocolNo: 'P-1', protocolStatus: 'sxz' },
|
||||
@@ -64,40 +64,66 @@ test('protocol detail exposes reception qc production storage and release inform
|
||||
const labels = [...sample.fields.map((field) => field.label), ...sample.cards.flatMap(cardLabels)]
|
||||
for (const expected of [
|
||||
'质检接收', '接收时间', '接收附件', '生产接收', '处理时间',
|
||||
'门控质检', '工单号', '过程质检 1', '生产信息', '生产任务 1',
|
||||
'入库放行', '生产结论', '质检结论', '入库状态', '质检附件', '生产附件'
|
||||
]) assert.ok(labels.includes(expected), `missing ${expected}`)
|
||||
for (const hidden of ['门控质检', '过程质检 1', '生产信息', '生产任务 1']) {
|
||||
assert.ok(!labels.includes(hidden), `must not show ${hidden}`)
|
||||
}
|
||||
assert.equal(sections.find((section) => section.key === 'storageCert'), undefined)
|
||||
assert.equal(sections.find((section) => section.key === 'insurance'), undefined)
|
||||
})
|
||||
|
||||
test('protocol lifecycle keeps qc targets results batches and production inputs outputs', () => {
|
||||
test('protocol detail shows refund registration attachments', () => {
|
||||
const files = [{ fileUrl: '/minio/refund.pdf', fileName: '退费凭证.pdf' }]
|
||||
const sections = getMarketModule('protocol').detailSections({
|
||||
source: 'research_donation',
|
||||
basic: { donationNo: 'D-1', donationStatus: 'sxz' },
|
||||
samples: [{
|
||||
id: 'sample-rich', sampleCode: 'D-S-1',
|
||||
qcInfo: {
|
||||
workOrderNo: 'QC-1', cellProductCode: 'CP-1', cellProductName: '细胞产品',
|
||||
productionWorkOrderNo: 'PR-1', triggerSourceLabel: '样本接收',
|
||||
inspectionBatches: [{ id: 'b1', batchNo: 'B-1', batchName: '第一批', assigneeUserNames: '质检员', taskCount: 1, completedTaskCount: 1 }],
|
||||
tasks: [{ taskNo: 'QT-1', results: [{ paramName: '活率', paramUnit: '%', paramValue: '98' }] }]
|
||||
},
|
||||
productionInfo: {
|
||||
workOrderNo: 'PR-1',
|
||||
tasks: [{
|
||||
taskNo: 'PT-1',
|
||||
inputs: [{ inputType: '原料', inputCode: 'IN-1', inputName: '脐带血' }],
|
||||
outputs: [{ outputType: '产物', outputCode: 'OUT-1', outputName: '细胞制剂' }]
|
||||
}]
|
||||
}
|
||||
source: 'cell_storage',
|
||||
basic: { protocolNo: 'P-1', protocolStatus: 'sxz' },
|
||||
refundRecords: [{
|
||||
id: 'rr-1',
|
||||
refundAmount: 1200,
|
||||
refundTime: '2026-10-16 09:00:00',
|
||||
refundRemark: '客户申请',
|
||||
refundAttachments: '/minio/refund.pdf',
|
||||
refundAttachmentFiles: files
|
||||
}]
|
||||
})
|
||||
const sample = sections.find((section) => section.key === 'samples').cards[0]
|
||||
const labels = [...sample.cards.flatMap(cardLabels)]
|
||||
for (const expected of [
|
||||
'质检对象编码', '质检对象名称', '生产工单号', '触发来源',
|
||||
'检测批次 1', '批次编号', '负责人', '检测结果 1', '参数值',
|
||||
'投入物 1', '投入物编码', '产出物 1', '产出物编码'
|
||||
]) assert.ok(labels.includes(expected), `missing ${expected}`)
|
||||
const refund = sections.find((section) => section.key === 'refundRecords')
|
||||
assert.equal(refund.title, '退费登记')
|
||||
assert.equal(refund.cards.length, 1)
|
||||
assert.equal(refund.cards[0].fields.find((field) => field.label === '退费金额').value, '1200')
|
||||
assert.deepEqual(
|
||||
refund.cards[0].fields.find((field) => field.type === 'attachments').value,
|
||||
files
|
||||
)
|
||||
})
|
||||
|
||||
test('donation detail follows the web view and omits price and payment', () => {
|
||||
const sections = getMarketModule('protocol').detailSections({
|
||||
source: 'research_donation',
|
||||
basic: { donationNo: 'D-1', donationStatus: 'sxz', donationDate: '20261001', remark: '捐赠备注' },
|
||||
customer: { motherName: '李四' },
|
||||
samples: [{
|
||||
id: 's1', sampleCode: 'D-S-1', sampleTypeName: '脐带血',
|
||||
price: 100, originalPrice: 200, storageYears: 20
|
||||
}],
|
||||
payment: { payMode: 'dj', contractAmount: 100 },
|
||||
samplePayments: [{ sampleCode: 'D-S-1', payMode: 'dj', contractAmount: 100 }],
|
||||
attachments: { protocolAttachments: 'a.pdf', healthSurveyFormUrls: 'h.pdf', consentFormUrls: 'c.pdf' }
|
||||
})
|
||||
assert.deepEqual(sections.map((section) => section.key), [
|
||||
'protocol', 'collection', 'samples', 'customer', 'audits', 'attachments', 'statusChange'
|
||||
])
|
||||
assert.equal(sections[0].title, '基本信息')
|
||||
assert.deepEqual(sections[0].fields.map((field) => field.label), [
|
||||
'状态', '母亲姓名', '捐赠日期', '创建时间', '备注'
|
||||
])
|
||||
const sampleLabels = sections.find((section) => section.key === 'samples').cards[0].fields.map((field) => field.label)
|
||||
for (const hidden of ['优惠价', '原价', '存储年限']) {
|
||||
assert.ok(!sampleLabels.includes(hidden), `must not show ${hidden}`)
|
||||
}
|
||||
assert.equal(sections.find((section) => section.key === 'payment'), undefined)
|
||||
const attachmentLabels = sections.find((section) => section.key === 'attachments').fields.map((field) => field.label)
|
||||
assert.deepEqual(attachmentLabels, ['知情同意书', '传染病检测报告', '其他附件'])
|
||||
})
|
||||
|
||||
test('donation detail shows audit history and convert-to-storage status history', () => {
|
||||
@@ -112,7 +138,28 @@ test('donation detail shows audit history and convert-to-storage status history'
|
||||
const audit = sections.find((section) => section.key === 'audits')
|
||||
assert.equal(audit.cards.length, 1)
|
||||
const status = sections.find((section) => section.key === 'statusChange')
|
||||
assert.deepEqual(status.fields.map((field) => field.label), ['转细胞存储时间', '转细胞存储描述'])
|
||||
assert.equal(status.cards.length, 1)
|
||||
assert.equal(status.cards[0].statusText, '转细胞存储')
|
||||
assert.equal(
|
||||
status.cards[0].fields.find((field) => field.label === '变更说明').value,
|
||||
'已转细胞存储'
|
||||
)
|
||||
})
|
||||
|
||||
test('status change history renders one card per record including same type', () => {
|
||||
const sections = getMarketModule('protocol').detailSections({
|
||||
source: 'cell_storage',
|
||||
basic: { protocolNo: 'P-1', protocolStatus: 'ygq' },
|
||||
statusChanges: [
|
||||
{ id: 's1', changeType: 'expire', fromStatus: 'sxz', toStatus: 'ygq', changeTime: '2026-06-01 12:00:00', changeDesc: '第一次过期', operatorUserName: '李四' },
|
||||
{ id: 's2', changeType: 'expire', fromStatus: 'ygq', toStatus: 'ygq', changeTime: '2026-09-15 18:00:00', changeDesc: '第二次过期', operatorUserName: '王五' }
|
||||
]
|
||||
})
|
||||
const status = sections.find((section) => section.key === 'statusChange')
|
||||
assert.equal(status.cards.length, 2)
|
||||
assert.equal(status.cards[0].statusText, '过期')
|
||||
assert.equal(status.cards[1].fields.find((field) => field.label === '变更说明').value, '第二次过期')
|
||||
assert.equal(status.cards[0].fields.find((field) => field.label === '状态流转').value, '生效中 → 已过期')
|
||||
})
|
||||
|
||||
test('v1 protocol detail renders readonly payment details with backend transfer labels', () => {
|
||||
@@ -263,11 +310,14 @@ test('protocol transfer controls require backend nested capability and frontend
|
||||
|
||||
test('protocol transfer UI calls DELETE for persisted rows without reloading form drafts', () => {
|
||||
const panel = source('./components/ProtocolSamplePaymentPanel.vue')
|
||||
const editor = source('./components/ScheduleTransferEditor.vue')
|
||||
const form = source('../views/market/MarketModuleForm.vue')
|
||||
assert.doesNotMatch(form, /:can-manage-transfers="true"/)
|
||||
assert.match(panel, /performMarketAction\(paymentContext\(\), 'delete-payment-transfer'/)
|
||||
assert.match(panel, /title:\s*'提交财务确认'/)
|
||||
assert.match(panel, /<ScheduleTransferEditor/)
|
||||
assert.match(editor, /performMarketAction\(paymentContext\(\), 'delete-payment-transfer'/)
|
||||
assert.match(editor, /title:\s*'提交财务确认'/)
|
||||
assert.doesNotMatch(panel, /emit\('refresh'\)/)
|
||||
assert.doesNotMatch(editor, /emit\('refresh'\)/)
|
||||
assert.doesNotMatch(form, /@refresh="handleTransferRefresh"/)
|
||||
assert.match(form, /@transfer-dirty-change="handleTransferDirtyChange"/)
|
||||
assert.match(form, /protocolTransferSaveBlockMessage\(transferDirty\.value\)/)
|
||||
@@ -294,13 +344,14 @@ test('protocol uploads use the Web 20MB rich attachment policy', () => {
|
||||
test('protocol list and nested payment actions use OA-aligned soft semantic buttons', () => {
|
||||
const styles = source('../styles/market.css')
|
||||
const panel = source('./components/ProtocolSamplePaymentPanel.vue')
|
||||
const editor = source('./components/ScheduleTransferEditor.vue')
|
||||
assert.match(styles, /\.market-list-card__actions \.van-button\s*\{[^}]*min-height:\s*32px;/)
|
||||
assert.match(styles, /\.market-list-card__actions \.van-button\s*\{[\s\S]*?padding:\s*0 12px;/)
|
||||
assert.match(styles, /\.market-page \.van-button--mini\s*\{[\s\S]*?min-height:\s*28px;/)
|
||||
assert.match(styles, /\.market-page \.van-button--mini\s*\{[\s\S]*?border-radius:\s*8px;/)
|
||||
assert.match(styles, /\.market-page \.van-button--primary\s*\{[\s\S]*?background:\s*var\(--market-primary\);/)
|
||||
assert.doesNotMatch(styles, /linear-gradient\(135deg,\s*#12b981/)
|
||||
assert.match(panel, /\.protocol-sample-pay__transfer-actions[\s\S]*?\.van-button[\s\S]*?min-height:\s*28px;/)
|
||||
assert.match(editor, /\.protocol-sample-pay__transfer-actions[\s\S]*?\.van-button[\s\S]*?min-height:\s*28px;/)
|
||||
assert.match(panel, /protocol-sample-pay__timeline/)
|
||||
assert.match(panel, /protocol-sample-pay__sum/)
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user