refactor(mobile-oa-h5): extract ScheduleTransferEditor for shared transfer UI

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-10-17 11:04:14 +08:00
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/)
})