1015 lines
32 KiB
Vue
1015 lines
32 KiB
Vue
<!-- 预签约订单 新增/编辑 -->
|
||
<!-- eslint-disable vue/no-v-model-argument -->
|
||
<template>
|
||
<el-dialog
|
||
:model-value="modelValue"
|
||
:title="orderId ? '编辑预签约订单' : '新增预签约订单'"
|
||
width="1200px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
append-to-body
|
||
modal-class="protocol-dialog-mask"
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
@update:model-value="emit('update:modelValue', $event)"
|
||
@closed="resetForm"
|
||
>
|
||
<DialogFormBody v-loading="loading">
|
||
<el-form ref="formRef" :model="form" label-position="top" scroll-to-error>
|
||
<div class="form-grid-3">
|
||
<el-form-item label="业务日期">
|
||
<el-date-picker
|
||
v-model="form.orderDate"
|
||
type="date"
|
||
value-format="YYYYMMDD"
|
||
format="YYYY-MM-DD"
|
||
clearable
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="协议编号">
|
||
<el-input v-model="form.protocolNo" placeholder="可选,转正式时可预占" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="母亲姓名" required>
|
||
<el-input v-model="form.motherName" placeholder="请输入母亲姓名" clearable />
|
||
</el-form-item>
|
||
<el-form-item label="预产期">
|
||
<el-date-picker
|
||
v-model="form.expectedDeliveryDate"
|
||
type="date"
|
||
value-format="YYYYMMDD"
|
||
format="YYYY-MM-DD"
|
||
clearable
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="采集地区">
|
||
<el-select
|
||
v-model="form.regionId"
|
||
filterable
|
||
clearable
|
||
placeholder="请选择"
|
||
style="width: 100%"
|
||
@change="onRegionChange"
|
||
>
|
||
<el-option
|
||
v-for="opt in regionOptions"
|
||
:key="opt.id"
|
||
:label="opt.regionName"
|
||
:value="opt.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="医院">
|
||
<el-select
|
||
v-model="form.hospitalId"
|
||
filterable
|
||
clearable
|
||
placeholder="请选择"
|
||
style="width: 100%"
|
||
:disabled="!form.regionId"
|
||
>
|
||
<el-option
|
||
v-for="opt in hospitalOptions"
|
||
:key="opt.id"
|
||
:label="opt.hospitalName"
|
||
:value="opt.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="采集专员">
|
||
<el-select
|
||
v-model="form.collectorUserId"
|
||
class="w-full"
|
||
filterable
|
||
remote
|
||
clearable
|
||
reserve-keyword
|
||
:remote-method="remoteSearchCollectorUsers"
|
||
:loading="collectorLoading"
|
||
:disabled="isConverted"
|
||
placeholder="输入姓名检索用户"
|
||
style="width: 100%"
|
||
@change="onCollectorUserChange"
|
||
>
|
||
<el-option
|
||
v-for="u in collectorUserOptions"
|
||
:key="u.id"
|
||
:label="u.nickName + (u.phoneNo ? ' · ' + u.phoneNo : '')"
|
||
:value="u.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="采集经理">
|
||
<el-select
|
||
v-model="form.regionManagerUserId"
|
||
class="w-full"
|
||
filterable
|
||
remote
|
||
clearable
|
||
reserve-keyword
|
||
:remote-method="remoteSearchManagerUsers"
|
||
:loading="managerLoading"
|
||
:disabled="isConverted"
|
||
placeholder="默认取地区负责人,可修改"
|
||
style="width: 100%"
|
||
@change="onManagerUserChange"
|
||
>
|
||
<el-option
|
||
v-for="u in managerUserOptions"
|
||
:key="u.id"
|
||
:label="u.nickName + (u.phoneNo ? ' · ' + u.phoneNo : '')"
|
||
:value="u.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="采集时间">
|
||
<el-date-picker
|
||
v-model="form.sampleCollectTime"
|
||
type="datetime"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
clearable
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="缴费方式">
|
||
<el-select
|
||
v-model="form.payMode"
|
||
clearable
|
||
placeholder="整单初步方式"
|
||
style="width: 100%"
|
||
>
|
||
<el-option label="趸交" value="dj" />
|
||
<el-option label="分期" value="fq" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="整单合同金额(元)">
|
||
<AmountInput v-model="form.contractAmount" :min="0" style="width: 100%" />
|
||
</el-form-item>
|
||
<el-form-item label="订单状态">
|
||
<el-select v-model="form.orderStatus" style="width: 100%" :disabled="isConverted">
|
||
<el-option label="草稿" value="draft" />
|
||
<el-option label="进行中" value="active" />
|
||
<el-option label="已转正式" value="converted" />
|
||
<el-option label="已作废" value="void" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="样本类型" required>
|
||
<el-select
|
||
v-model="form.sampleTypeIds"
|
||
multiple
|
||
filterable
|
||
clearable
|
||
placeholder="可多选样本类型"
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="opt in sampleTypeOptions"
|
||
:key="opt.id"
|
||
:label="opt.typeName || opt.sampleTypeName"
|
||
:value="opt.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input
|
||
v-model="form.remark"
|
||
maxlength="500"
|
||
show-word-limit
|
||
placeholder="选填"
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
</div>
|
||
|
||
<div class="section-subtitle">意向金转账</div>
|
||
<div class="transfer-toolbar">
|
||
<el-button type="primary" size="small" :disabled="isConverted" @click="addTransfer">
|
||
<el-icon><Plus /></el-icon>
|
||
添加转账笔
|
||
</el-button>
|
||
<span v-if="confirmedDeposit > 0" class="deposit-hint">
|
||
已确认意向金合计 ¥{{ formatMoneyDisplay(confirmedDeposit) }}
|
||
</span>
|
||
</div>
|
||
<el-table
|
||
:data="form.transfers"
|
||
class="list-table transfer-table"
|
||
stripe
|
||
empty-text="暂无意向金转账笔,请点击上方按钮添加"
|
||
>
|
||
<el-table-column type="index" label="#" min-width="50" align="center" />
|
||
<el-table-column label="转账方式" min-width="140" align="center">
|
||
<template #default="{ row }">
|
||
<el-select
|
||
v-model="row.transferType"
|
||
:disabled="isTransferLocked(row) || isConverted"
|
||
placeholder="请选择"
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="opt in transferTypeOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="转账金额(元)" min-width="140" align="center">
|
||
<template #default="{ row }">
|
||
<AmountInput
|
||
v-model="row.transferAmount"
|
||
:min="0"
|
||
:disabled="isTransferLocked(row) || isConverted"
|
||
style="width: 100%"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="转账时间" min-width="190" align="center">
|
||
<template #default="{ row }">
|
||
<el-date-picker
|
||
v-model="row.transferTime"
|
||
type="datetime"
|
||
format="YYYY-MM-DD HH:mm"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
:disabled="isTransferLocked(row) || isConverted"
|
||
clearable
|
||
placeholder="选择时间"
|
||
style="width: 100%"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="转账凭证" min-width="280" align="left">
|
||
<template #default="{ row, $index }">
|
||
<div class="attachment-upload">
|
||
<div class="attachment-list">
|
||
<el-upload
|
||
v-if="!isTransferLocked(row) && !isConverted"
|
||
:auto-upload="false"
|
||
:show-file-list="false"
|
||
:multiple="true"
|
||
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||
@change="(f: any) => handleTransferAttachmentChange($index, f)"
|
||
>
|
||
<div class="upload-card">
|
||
<el-icon :size="22" color="#047857"><Plus /></el-icon>
|
||
<span class="upload-card-text">上传凭证</span>
|
||
</div>
|
||
</el-upload>
|
||
<div
|
||
v-for="(file, fIdx) in row.attachments || []"
|
||
:key="file.id || fIdx"
|
||
class="attachment-item"
|
||
:class="{ 'is-uploading': file._uploading }"
|
||
>
|
||
<div class="attachment-preview">
|
||
<template v-if="file.fileUrl && isImageUrl(file.fileUrl)">
|
||
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
|
||
</template>
|
||
<template v-else>
|
||
<div class="attachment-preview__icon">
|
||
<el-icon :size="22"><Document /></el-icon>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div class="attachment-footer">
|
||
<span class="attachment-name" :title="file.fileName">{{
|
||
file.fileName || '附件'
|
||
}}</span>
|
||
<el-icon
|
||
v-if="!isTransferLocked(row) && !isConverted"
|
||
class="action-btn"
|
||
title="删除"
|
||
@click="removeTransferAttachment($index, fIdx)"
|
||
>
|
||
<Delete />
|
||
</el-icon>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" min-width="160" align="center">
|
||
<template #default="{ row }">
|
||
<el-input
|
||
v-if="!isTransferLocked(row) && !isConverted"
|
||
v-model="row.remark"
|
||
maxlength="512"
|
||
clearable
|
||
placeholder="选填"
|
||
/>
|
||
<span v-else>{{ row.remark || '—' }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
label="财务备注"
|
||
min-width="140"
|
||
align="center"
|
||
show-overflow-tooltip
|
||
>
|
||
<template #default="{ row }">{{ row.financeRemark || '—' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" min-width="110" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="payStatusTagType(String(row.payStatus || 'dfj'))" size="small">
|
||
{{ payStatusLabel(String(row.payStatus || 'dfj')) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" min-width="220" align="center" fixed="right">
|
||
<template #default="{ row, $index }">
|
||
<el-button
|
||
v-if="row.id && row.payStatus === 'dfj' && !isConverted"
|
||
link
|
||
type="primary"
|
||
size="small"
|
||
@click="submitTransfer(row)"
|
||
>
|
||
<el-icon><Check /></el-icon>
|
||
提交财务确认
|
||
</el-button>
|
||
<el-button
|
||
v-if="row.id && row.payStatus === 'jfqrz' && !isConverted"
|
||
link
|
||
type="warning"
|
||
size="small"
|
||
@click="withdrawTransfer(row)"
|
||
>
|
||
<el-icon><RefreshLeft /></el-icon>
|
||
撤回
|
||
</el-button>
|
||
<el-button
|
||
v-if="!isTransferLocked(row) && !isConverted"
|
||
link
|
||
type="danger"
|
||
size="small"
|
||
@click="form.transfers.splice($index, 1)"
|
||
>
|
||
<el-icon><Delete /></el-icon>
|
||
删除
|
||
</el-button>
|
||
<span
|
||
v-if="
|
||
isConverted ||
|
||
(isTransferLocked(row) &&
|
||
!(row.id && (row.payStatus === 'dfj' || row.payStatus === 'jfqrz')))
|
||
"
|
||
class="muted-dash"
|
||
>—</span
|
||
>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-form>
|
||
</DialogFormBody>
|
||
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button
|
||
class="btn-main"
|
||
type="primary"
|
||
:loading="saving"
|
||
:disabled="isConverted"
|
||
@click="handleSave"
|
||
>
|
||
<el-icon><Check /></el-icon>
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, reactive, ref, watch } from 'vue'
|
||
import { Plus, Close, Check, Document, Delete, RefreshLeft } from '@element-plus/icons-vue'
|
||
import AmountInput from '@/components/common/amount-input/index.vue'
|
||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||
import {
|
||
payStatusLabel,
|
||
payStatusTagType,
|
||
loadTransferTypeDict,
|
||
getTransferTypeOptions
|
||
} from '@/utils/protocol-payment-labels'
|
||
import DialogFormBody from '@/components/business/dialog-form-body/index.vue'
|
||
import { useUserStore } from '@/store/modules/user'
|
||
import { fetchUploadAttachment } from '@/api/business'
|
||
import {
|
||
fetchCellSampleTypeList,
|
||
fetchCollectionRegionList,
|
||
fetchHospitalList,
|
||
fetchRegionStaffList,
|
||
fetchUserList
|
||
} from '@/api/admin'
|
||
import {
|
||
fetchPresignOrderDetail,
|
||
fetchCreatePresignOrder,
|
||
fetchUpdatePresignOrder,
|
||
fetchCreatePresignTransfer,
|
||
fetchUpdatePresignTransfer,
|
||
fetchSubmitPresignTransferFinance,
|
||
fetchWithdrawPresignTransfer
|
||
} from '@/api/presign-order'
|
||
|
||
const userStore = useUserStore()
|
||
|
||
const props = defineProps<{
|
||
modelValue: boolean
|
||
orderId?: string
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
'update:modelValue': [boolean]
|
||
saved: []
|
||
}>()
|
||
|
||
const formRef = ref()
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const transferTypeOptions = ref<Api.Dict.DictDataOption[]>([])
|
||
const sampleTypeOptions = ref<Array<Record<string, any>>>([])
|
||
const regionOptions = ref<Array<Record<string, any>>>([])
|
||
const hospitalOptions = ref<Array<Record<string, any>>>([])
|
||
const collectorUserOptions = ref<Api.User.UserItem[]>([])
|
||
const managerUserOptions = ref<Api.User.UserItem[]>([])
|
||
const collectorLoading = ref(false)
|
||
const managerLoading = ref(false)
|
||
|
||
function todayYmd() {
|
||
const d = new Date()
|
||
const y = d.getFullYear()
|
||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||
const day = String(d.getDate()).padStart(2, '0')
|
||
return `${y}${m}${day}`
|
||
}
|
||
|
||
function userStubFromStore(): Api.User.UserItem | null {
|
||
const currentUser = userStore.getUserInfo
|
||
const uid = String(currentUser?.id || currentUser?.userId || '')
|
||
if (!uid) return null
|
||
return {
|
||
id: uid,
|
||
nickName: String(
|
||
currentUser?.realName || currentUser?.userName || currentUser?.username || ''
|
||
),
|
||
phoneNo: String(currentUser?.phone || currentUser?.phoneNo || ''),
|
||
deptId: '',
|
||
userNo: '',
|
||
email: '',
|
||
sex: '',
|
||
avatar: '',
|
||
status: '1',
|
||
tenantNo: '',
|
||
orderNum: 0,
|
||
remark: '',
|
||
createTime: '',
|
||
updateTime: ''
|
||
} as Api.User.UserItem
|
||
}
|
||
|
||
function makeUserStub(id: string, name: string): Api.User.UserItem {
|
||
return {
|
||
id,
|
||
nickName: name || id,
|
||
phoneNo: '',
|
||
deptId: '',
|
||
userNo: '',
|
||
email: '',
|
||
sex: '',
|
||
avatar: '',
|
||
status: '1',
|
||
tenantNo: '',
|
||
orderNum: 0,
|
||
remark: '',
|
||
createTime: '',
|
||
updateTime: ''
|
||
} as Api.User.UserItem
|
||
}
|
||
|
||
const form = reactive({
|
||
orderDate: todayYmd(),
|
||
protocolNo: '',
|
||
motherName: '',
|
||
expectedDeliveryDate: '',
|
||
regionId: '',
|
||
hospitalId: '',
|
||
collectorUserId: '',
|
||
regionManagerUserId: '',
|
||
sampleCollectTime: '',
|
||
payMode: 'dj',
|
||
contractAmount: null as number | null,
|
||
orderStatus: 'active',
|
||
sampleTypeIds: [] as string[],
|
||
remark: '',
|
||
transfers: [] as Array<Record<string, any>>,
|
||
confirmedDepositAmount: 0
|
||
})
|
||
|
||
const isConverted = computed(() => form.orderStatus === 'converted')
|
||
const confirmedDeposit = computed(() => Number(form.confirmedDepositAmount || 0))
|
||
|
||
function isTransferLocked(tr: Record<string, any>) {
|
||
return tr.payStatus === 'jfqrz' || tr.payStatus === 'yjf'
|
||
}
|
||
|
||
type TransferAttachment = {
|
||
id?: string
|
||
fileName?: string
|
||
fileUrl?: string
|
||
_uploading?: boolean
|
||
}
|
||
|
||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||
|
||
function fullFileUrl(url: string) {
|
||
if (!url) return ''
|
||
if (/^https?:\/\//i.test(url)) return url
|
||
return minioBaseUrl ? `${minioBaseUrl}/${url.replace(/^\//, '')}` : url
|
||
}
|
||
|
||
function isImageUrl(url: string) {
|
||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i.test(url)
|
||
}
|
||
|
||
function attachmentsToUrls(files: TransferAttachment[] | undefined) {
|
||
return (files || [])
|
||
.filter((f) => f.fileUrl && !f._uploading)
|
||
.map((f) => String(f.fileUrl))
|
||
.join(',')
|
||
}
|
||
|
||
function normalizeTransfer(row: Record<string, any>) {
|
||
const files = Array.isArray(row.attachments) ? row.attachments : []
|
||
return {
|
||
...row,
|
||
attachments: files.map((f: Record<string, any>, i: number) => ({
|
||
id: f.id || `att_${i}`,
|
||
fileName: f.fileName || f.name || '附件',
|
||
fileUrl: f.fileUrl || f.url || ''
|
||
})) as TransferAttachment[]
|
||
}
|
||
}
|
||
|
||
async function handleTransferAttachmentChange(index: number, uploadFile: { raw?: File }) {
|
||
const file = uploadFile.raw
|
||
if (!file) return
|
||
if (file.size > 20 * 1024 * 1024) {
|
||
ElMessage.warning('单文件不超过 20MB')
|
||
return
|
||
}
|
||
const row = form.transfers[index]
|
||
if (!row) return
|
||
if (!Array.isArray(row.attachments)) row.attachments = []
|
||
const tempId = `temp_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||
row.attachments.push({
|
||
id: tempId,
|
||
fileName: file.name,
|
||
fileUrl: '',
|
||
_uploading: true
|
||
})
|
||
const idx = row.attachments.length - 1
|
||
try {
|
||
const fd = new FormData()
|
||
fd.append('folder', 'presign-transfer')
|
||
fd.append('file', file)
|
||
const res: any = await fetchUploadAttachment(fd)
|
||
const data = res?.data || res
|
||
row.attachments[idx] = {
|
||
id: data?.id || tempId,
|
||
fileName: data?.fileName || file.name,
|
||
fileUrl: data?.fileUrl || data?.url || '',
|
||
_uploading: false
|
||
}
|
||
} catch {
|
||
ElMessage.error('文件上传失败')
|
||
row.attachments.splice(idx, 1)
|
||
}
|
||
}
|
||
|
||
function removeTransferAttachment(rowIndex: number, fileIndex: number) {
|
||
const row = form.transfers[rowIndex]
|
||
if (!row?.attachments) return
|
||
row.attachments.splice(fileIndex, 1)
|
||
}
|
||
|
||
function applyDefaultCollector() {
|
||
const stub = userStubFromStore()
|
||
if (!stub) {
|
||
form.collectorUserId = ''
|
||
collectorUserOptions.value = []
|
||
return
|
||
}
|
||
form.collectorUserId = stub.id
|
||
collectorUserOptions.value = [stub]
|
||
}
|
||
|
||
function resetForm() {
|
||
form.orderDate = todayYmd()
|
||
form.protocolNo = ''
|
||
form.motherName = ''
|
||
form.expectedDeliveryDate = ''
|
||
form.regionId = ''
|
||
form.hospitalId = ''
|
||
form.collectorUserId = ''
|
||
form.regionManagerUserId = ''
|
||
form.sampleCollectTime = ''
|
||
form.payMode = 'dj'
|
||
form.contractAmount = null
|
||
form.orderStatus = 'active'
|
||
form.sampleTypeIds = []
|
||
form.remark = ''
|
||
form.transfers = []
|
||
form.confirmedDepositAmount = 0
|
||
collectorUserOptions.value = []
|
||
managerUserOptions.value = []
|
||
applyDefaultCollector()
|
||
}
|
||
|
||
async function remoteSearchCollectorUsers(keyword: string) {
|
||
collectorLoading.value = true
|
||
try {
|
||
const res = await fetchUserList({
|
||
page: 1,
|
||
pageSize: 50,
|
||
keyword: (keyword || '').trim() || undefined,
|
||
status: '1'
|
||
})
|
||
let list = res.list || []
|
||
const cur = form.collectorUserId
|
||
if (cur && !list.some((x) => x.id === cur)) {
|
||
const keep = collectorUserOptions.value.find((x) => x.id === cur)
|
||
if (keep) list = [keep, ...list]
|
||
}
|
||
collectorUserOptions.value = list
|
||
} catch {
|
||
collectorUserOptions.value = []
|
||
} finally {
|
||
collectorLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function remoteSearchManagerUsers(keyword: string) {
|
||
managerLoading.value = true
|
||
try {
|
||
const res = await fetchUserList({
|
||
page: 1,
|
||
pageSize: 50,
|
||
keyword: (keyword || '').trim() || undefined,
|
||
status: '1'
|
||
})
|
||
let list = res.list || []
|
||
const cur = form.regionManagerUserId
|
||
if (cur && !list.some((x) => x.id === cur)) {
|
||
const keep = managerUserOptions.value.find((x) => x.id === cur)
|
||
if (keep) list = [keep, ...list]
|
||
}
|
||
managerUserOptions.value = list
|
||
} catch {
|
||
managerUserOptions.value = []
|
||
} finally {
|
||
managerLoading.value = false
|
||
}
|
||
}
|
||
|
||
function onCollectorUserChange(val: string | null) {
|
||
form.collectorUserId = val || ''
|
||
}
|
||
|
||
function onManagerUserChange(val: string | null) {
|
||
form.regionManagerUserId = val || ''
|
||
}
|
||
|
||
async function applyDefaultRegionManager(regionId: string) {
|
||
if (!regionId) {
|
||
form.regionManagerUserId = ''
|
||
managerUserOptions.value = []
|
||
return
|
||
}
|
||
try {
|
||
const res: any = await fetchRegionStaffList(regionId, { page: 1, pageSize: 100 })
|
||
const list = (res?.list || []) as Array<Record<string, any>>
|
||
const leader = list.find((x) => x.isLeader) || list[0]
|
||
if (!leader?.userId) {
|
||
form.regionManagerUserId = ''
|
||
return
|
||
}
|
||
const stub = makeUserStub(
|
||
String(leader.userId),
|
||
String(leader.nickName || leader.userName || '')
|
||
)
|
||
form.regionManagerUserId = stub.id
|
||
if (!managerUserOptions.value.some((x) => x.id === stub.id)) {
|
||
managerUserOptions.value = [stub, ...managerUserOptions.value]
|
||
}
|
||
} catch {
|
||
/* 地区无人员配置时留空,可手工选择 */
|
||
}
|
||
}
|
||
|
||
function addTransfer() {
|
||
form.transfers.push({
|
||
transferType: '',
|
||
transferAmount: null,
|
||
transferTime: '',
|
||
payStatus: 'dfj',
|
||
remark: '',
|
||
financeRemark: '',
|
||
attachments: [] as TransferAttachment[]
|
||
})
|
||
}
|
||
|
||
async function loadLookups() {
|
||
await loadTransferTypeDict()
|
||
transferTypeOptions.value = getTransferTypeOptions()
|
||
const [types, regions] = await Promise.all([
|
||
fetchCellSampleTypeList({ page: 1, pageSize: 100 }),
|
||
fetchCollectionRegionList({ page: 1, pageSize: 100 })
|
||
])
|
||
sampleTypeOptions.value = (types as any)?.list || (types as any) || []
|
||
regionOptions.value = (regions as any)?.list || (regions as any) || []
|
||
}
|
||
|
||
async function loadHospitals() {
|
||
hospitalOptions.value = []
|
||
if (!form.regionId) return
|
||
const res: any = await fetchHospitalList(form.regionId, { page: 1, pageSize: 100 })
|
||
hospitalOptions.value = res?.list || []
|
||
}
|
||
|
||
async function onRegionChange() {
|
||
form.hospitalId = ''
|
||
await Promise.all([loadHospitals(), applyDefaultRegionManager(form.regionId)])
|
||
}
|
||
|
||
async function loadDetail() {
|
||
if (!props.orderId) return
|
||
loading.value = true
|
||
try {
|
||
const data: any = await fetchPresignOrderDetail(props.orderId)
|
||
form.orderDate = String(data.orderDate || '')
|
||
form.protocolNo = String(data.protocolNo || '')
|
||
form.motherName = String(data.motherName || '')
|
||
form.expectedDeliveryDate = String(data.expectedDeliveryDate || '')
|
||
form.regionId = String(data.regionId || '')
|
||
form.hospitalId = String(data.hospitalId || '')
|
||
form.collectorUserId = String(data.collectorUserId || '')
|
||
form.regionManagerUserId = String(data.regionManagerUserId || '')
|
||
form.sampleCollectTime = String(data.sampleCollectTime || '')
|
||
form.payMode = String(data.payMode || 'dj')
|
||
form.contractAmount = data.contractAmount ?? null
|
||
form.orderStatus = String(data.orderStatus || 'active')
|
||
form.sampleTypeIds = Array.isArray(data.sampleTypeIds) ? [...data.sampleTypeIds] : []
|
||
form.remark = String(data.remark || '')
|
||
form.transfers = Array.isArray(data.transfers)
|
||
? data.transfers.map((t: Record<string, any>) => normalizeTransfer(t))
|
||
: []
|
||
form.confirmedDepositAmount = Number(data.confirmedDepositAmount || 0)
|
||
if (form.collectorUserId) {
|
||
collectorUserOptions.value = [
|
||
makeUserStub(form.collectorUserId, String(data.collectorUserName || ''))
|
||
]
|
||
}
|
||
if (form.regionManagerUserId) {
|
||
managerUserOptions.value = [
|
||
makeUserStub(form.regionManagerUserId, String(data.regionManagerUserName || ''))
|
||
]
|
||
}
|
||
if (form.regionId) await loadHospitals()
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function submitTransfer(tr: Record<string, any>) {
|
||
if (!props.orderId || !tr.id) {
|
||
ElMessage.warning('请先保存订单后再提交财务确认')
|
||
return
|
||
}
|
||
if (tr.payStatus === 'dfj') {
|
||
await fetchUpdatePresignTransfer(String(tr.id), {
|
||
transferType: tr.transferType,
|
||
transferAmount: tr.transferAmount,
|
||
transferTime: tr.transferTime || undefined,
|
||
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
|
||
remark: tr.remark || undefined
|
||
})
|
||
}
|
||
await fetchSubmitPresignTransferFinance(String(tr.id))
|
||
tr.payStatus = 'jfqrz'
|
||
ElMessage.success('已提交财务确认')
|
||
}
|
||
|
||
async function withdrawTransfer(tr: Record<string, any>) {
|
||
if (!tr.id) return
|
||
await ElMessageBox.confirm(
|
||
'确认撤回该笔意向金的财务确认申请?撤回后可修改并重新提交。',
|
||
'撤回',
|
||
{ type: 'warning' }
|
||
)
|
||
await fetchWithdrawPresignTransfer(String(tr.id))
|
||
tr.payStatus = 'dfj'
|
||
ElMessage.success('已撤回')
|
||
}
|
||
|
||
async function handleSave() {
|
||
if (!form.motherName.trim()) {
|
||
ElMessage.warning('请填写母亲姓名')
|
||
return
|
||
}
|
||
if (!form.sampleTypeIds.length) {
|
||
ElMessage.warning('请至少选择一个样本类型')
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const payload: Record<string, unknown> = {
|
||
orderDate: form.orderDate || undefined,
|
||
protocolNo: form.protocolNo || undefined,
|
||
motherName: form.motherName,
|
||
expectedDeliveryDate: form.expectedDeliveryDate || undefined,
|
||
regionId: form.regionId || undefined,
|
||
hospitalId: form.hospitalId || undefined,
|
||
sampleCollectTime: form.sampleCollectTime || undefined,
|
||
collectorUserId: form.collectorUserId || undefined,
|
||
regionManagerUserId: form.regionManagerUserId || undefined,
|
||
payMode: form.payMode || undefined,
|
||
contractAmount: form.contractAmount ?? undefined,
|
||
orderStatus: form.orderStatus || 'active',
|
||
sampleTypeIds: form.sampleTypeIds,
|
||
remark: form.remark || undefined
|
||
}
|
||
if (props.orderId) {
|
||
await fetchUpdatePresignOrder(props.orderId, payload)
|
||
for (const tr of form.transfers) {
|
||
if (!tr.id && tr.payStatus === 'dfj') {
|
||
await fetchCreatePresignTransfer(props.orderId, {
|
||
transferType: tr.transferType,
|
||
transferAmount: tr.transferAmount,
|
||
transferTime: tr.transferTime || undefined,
|
||
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
|
||
remark: tr.remark || undefined
|
||
})
|
||
} else if (tr.id && tr.payStatus === 'dfj') {
|
||
await fetchUpdatePresignTransfer(String(tr.id), {
|
||
transferType: tr.transferType,
|
||
transferAmount: tr.transferAmount,
|
||
transferTime: tr.transferTime || undefined,
|
||
transferAttachments: attachmentsToUrls(tr.attachments) || undefined,
|
||
remark: tr.remark || undefined
|
||
})
|
||
}
|
||
}
|
||
} else {
|
||
payload.transfers = form.transfers
|
||
.filter((t) => t.payStatus === 'dfj' || !t.payStatus)
|
||
.map((t) => ({
|
||
transferType: t.transferType,
|
||
transferAmount: t.transferAmount,
|
||
transferTime: t.transferTime || undefined,
|
||
transferAttachments: attachmentsToUrls(t.attachments) || undefined,
|
||
remark: t.remark || undefined
|
||
}))
|
||
await fetchCreatePresignOrder(payload)
|
||
}
|
||
emit('update:modelValue', false)
|
||
emit('saved')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
watch(
|
||
() => props.modelValue,
|
||
async (open) => {
|
||
if (!open) return
|
||
resetForm()
|
||
await loadLookups()
|
||
if (props.orderId) await loadDetail()
|
||
}
|
||
)
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.section-subtitle {
|
||
margin: 16px 0 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--mint-text-secondary, #475569);
|
||
}
|
||
|
||
.transfer-toolbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 8px 12px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.deposit-hint {
|
||
font-size: 12px;
|
||
color: #047857;
|
||
}
|
||
|
||
.transfer-table {
|
||
width: 100%;
|
||
|
||
:deep(.el-date-editor) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.muted-dash {
|
||
color: var(--mint-text-muted, #94a3b8);
|
||
}
|
||
|
||
.attachment-upload {
|
||
width: 100%;
|
||
}
|
||
|
||
.attachment-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.upload-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 88px;
|
||
height: 72px;
|
||
cursor: pointer;
|
||
border: 1px dashed #a7f3d0;
|
||
border-radius: 8px;
|
||
background: #ecfdf5;
|
||
}
|
||
|
||
.upload-card-text {
|
||
margin-top: 4px;
|
||
font-size: 11px;
|
||
color: #047857;
|
||
}
|
||
|
||
.attachment-item {
|
||
width: 88px;
|
||
overflow: hidden;
|
||
background: #fff;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 8px;
|
||
|
||
&.is-uploading {
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
.attachment-preview {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 88px;
|
||
height: 52px;
|
||
background: #f8fafc;
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
}
|
||
|
||
.attachment-preview__icon {
|
||
color: #64748b;
|
||
}
|
||
|
||
.attachment-footer {
|
||
display: flex;
|
||
gap: 4px;
|
||
align-items: center;
|
||
padding: 4px 6px;
|
||
}
|
||
|
||
.attachment-name {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.action-btn {
|
||
flex-shrink: 0;
|
||
cursor: pointer;
|
||
color: #94a3b8;
|
||
|
||
&:hover {
|
||
color: #dc2626;
|
||
}
|
||
}
|
||
</style>
|