提交
This commit is contained in:
1 parent
427bcb1551
commit
31b04575e2
247 files changed
+39284
-20367
No files matched your search
@@ -15,9 +15,14 @@
|
||||
<el-form-item label="物料筛选">
|
||||
<el-select
|
||||
v-model="searchForm.materialId"
|
||||
placeholder="全部物料"
|
||||
placeholder="输入物料名称检索"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:loading="materialOptionsLoading"
|
||||
:remote-method="remoteSearchMaterials"
|
||||
@visible-change="onMaterialSelectVisible"
|
||||
>
|
||||
<el-option
|
||||
v-for="m in materialOptions"
|
||||
@@ -281,7 +286,7 @@
|
||||
import { Search, RefreshLeft, View, Document, CircleCheck } from '@element-plus/icons-vue'
|
||||
import { fetchMaterialUsageList, fetchMarkMaterialUsageFullyUsed } from '@/api/material-usage'
|
||||
import type { MaterialUsage } from '@/api/material-usage'
|
||||
import { fetchMaterialOptions } from '@/api/material'
|
||||
import { fetchMaterialList } from '@/api/material'
|
||||
import type { MaterialOption } from '@/api/material'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
@@ -305,6 +310,7 @@
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const materialOptions = ref<MaterialOption[]>([])
|
||||
const materialOptionsLoading = ref(false)
|
||||
|
||||
function isManualOutRow(row: MaterialUsage) {
|
||||
if (row.sourceType === 'manual_out') return true
|
||||
@@ -410,15 +416,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMaterialOptions() {
|
||||
function mapMaterialListToOptions(list: Array<Record<string, any>>): MaterialOption[] {
|
||||
return (list || []).map((m) => ({
|
||||
id: String(m.id || ''),
|
||||
label: `${m.materialName || ''}${m.materialCode ? ` (${m.materialCode})` : ''}`.trim(),
|
||||
materialCode: String(m.materialCode || ''),
|
||||
materialName: String(m.materialName || ''),
|
||||
category: String(m.category || ''),
|
||||
specModel: String(m.specModel || ''),
|
||||
unit: String(m.unit || ''),
|
||||
stockQty: Number(m.stockQty ?? 0)
|
||||
}))
|
||||
}
|
||||
|
||||
async function remoteSearchMaterials(query: string) {
|
||||
materialOptionsLoading.value = true
|
||||
try {
|
||||
const res = await fetchMaterialOptions()
|
||||
materialOptions.value = res || []
|
||||
const res = await fetchMaterialList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
materialOptions.value = mapMaterialListToOptions(res.list || [])
|
||||
} catch {
|
||||
materialOptions.value = []
|
||||
} finally {
|
||||
materialOptionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onMaterialSelectVisible(visible: boolean) {
|
||||
if (visible) void remoteSearchMaterials('')
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -542,13 +572,13 @@
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadMaterialOptions()
|
||||
if (route.query.materialId) {
|
||||
searchForm.materialId = route.query.materialId as string
|
||||
}
|
||||
if (route.query.stockInNo) {
|
||||
searchForm.keyword = route.query.stockInNo as string
|
||||
}
|
||||
await remoteSearchMaterials('')
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -246,9 +246,15 @@
|
||||
<el-form-item label="保险公司" prop="insuranceCompanyId">
|
||||
<el-select
|
||||
v-model="formData.insuranceCompanyId"
|
||||
placeholder="请选择保险公司"
|
||||
placeholder="输入公司名称检索"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
clearable
|
||||
:loading="insuranceCompanyLoading"
|
||||
:remote-method="remoteSearchInsuranceCompanies"
|
||||
style="width: 100%"
|
||||
@visible-change="onInsuranceCompanySelectVisible"
|
||||
@change="handleInsuranceCompanyChange"
|
||||
>
|
||||
<el-option
|
||||
@@ -544,16 +550,41 @@
|
||||
// 保险公司下拉
|
||||
// ---------------------------------------------------------------------------
|
||||
const insuranceCompanyOptions = ref<any[]>([])
|
||||
const insuranceCompanyLoading = ref(false)
|
||||
|
||||
async function loadInsuranceCompanies() {
|
||||
async function remoteSearchInsuranceCompanies(query: string) {
|
||||
insuranceCompanyLoading.value = true
|
||||
try {
|
||||
const res = await fetchInsuranceCompanyList({ page: 1, pageSize: 100 })
|
||||
const res = await fetchInsuranceCompanyList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined,
|
||||
status: '1'
|
||||
})
|
||||
insuranceCompanyOptions.value = (res as any)?.list || []
|
||||
ensureInsuranceCompanyInOptions(formData.insuranceCompanyId)
|
||||
} catch {
|
||||
insuranceCompanyOptions.value = []
|
||||
ensureInsuranceCompanyInOptions(formData.insuranceCompanyId)
|
||||
} finally {
|
||||
insuranceCompanyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function ensureInsuranceCompanyInOptions(companyId: string) {
|
||||
const id = String(companyId || '').trim()
|
||||
if (!id || insuranceCompanyOptions.value.some((c) => c.id === id)) return
|
||||
insuranceCompanyOptions.value = [{ id, companyName: id }, ...insuranceCompanyOptions.value]
|
||||
}
|
||||
|
||||
function onInsuranceCompanySelectVisible(visible: boolean) {
|
||||
if (visible) void remoteSearchInsuranceCompanies('')
|
||||
}
|
||||
|
||||
async function loadInsuranceCompanies() {
|
||||
await remoteSearchInsuranceCompanies('')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 险种价格下拉(按保险公司联动)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -115,220 +115,181 @@
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="protocolStatus"
|
||||
label="状态"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
|
||||
size="small"
|
||||
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="expectedDeliveryDate"
|
||||
label="预产期"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sampleCollectSummary"
|
||||
label="是否已采集"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="registerDate"
|
||||
label="登记日期"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="motherName"
|
||||
label="母亲姓名"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="motherPhone"
|
||||
label="母亲电话"
|
||||
min-width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="motherIdCard"
|
||||
label="母亲身份证号"
|
||||
min-width="190"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="fatherName"
|
||||
label="父亲姓名"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="newbornSex"
|
||||
label="新生儿性别"
|
||||
min-width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{
|
||||
newbornSexLabel(String(row.newbornSex || ''))
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="deliveryMode"
|
||||
label="分娩方式"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{
|
||||
deliveryModeLabel(String(row.deliveryMode || ''))
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornWeightGram"
|
||||
label="新生儿体重(克)"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sampleTypeNames"
|
||||
label="样本类型"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sampleCodes"
|
||||
label="样本编码"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="labStorageCodes"
|
||||
label="实验室入库编码"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.labStorageCodes || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="regionName"
|
||||
label="采集地区"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="hospitalName"
|
||||
label="采集医院"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="collectorUserName"
|
||||
label="采集专员"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="salesUserName"
|
||||
label="销售人员"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sampleCollectTime"
|
||||
label="样本采集时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="160"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item" @click="openAuditDialog(row)">
|
||||
<el-icon><CircleCheck /></el-icon>审核
|
||||
</el-button>
|
||||
<el-dropdown trigger="hover" @command="(cmd: string) => handleDropdownCommand(cmd, row)">
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="view">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<div v-loading="loading" class="protocol-card-section">
|
||||
<div v-if="tableData.length" class="protocol-card-list">
|
||||
<article
|
||||
v-for="(row, index) in tableData"
|
||||
:key="String(row.id)"
|
||||
class="protocol-card"
|
||||
:class="{ 'is-selected': isRowSelected(String(row.id)) }"
|
||||
@dblclick="handleView(row)"
|
||||
>
|
||||
<div class="protocol-card__select" @click.stop>
|
||||
<el-checkbox
|
||||
:model-value="isRowSelected(String(row.id))"
|
||||
@change="(val) => toggleRowSelection(row, !!val)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="protocol-card__aside">
|
||||
<div class="protocol-card__name-row">
|
||||
<span class="protocol-card__index">{{
|
||||
(pagination.page - 1) * pagination.pageSize + index + 1
|
||||
}}</span>
|
||||
<span class="protocol-card__name">{{ row.motherName || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__contact">
|
||||
<div class="protocol-card__sub">{{ row.motherPhone || '—' }}</div>
|
||||
<div class="protocol-card__sub protocol-card__sub--id">
|
||||
{{ row.motherIdCard ? maskIdCard(String(row.motherIdCard)) : '—' }}
|
||||
</div>
|
||||
</div>
|
||||
<el-tag
|
||||
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
|
||||
size="small"
|
||||
class="protocol-card__status"
|
||||
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
|
||||
>
|
||||
<div v-if="row.createTime" class="protocol-card__create-time">
|
||||
创建 {{ row.createTime }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="protocol-card__sections">
|
||||
<section class="protocol-card__block protocol-card__block--basic">
|
||||
<div class="protocol-card__block-title">基础信息</div>
|
||||
<div class="protocol-card__kv-grid">
|
||||
<div class="protocol-card__kv">
|
||||
<label>协议号</label><span>{{ row.protocolNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>登记日期</label><span>{{ row.registerDate || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>采集地区</label><span>{{ row.regionName || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>采集医院</label
|
||||
><span :title="String(row.hospitalName || '')">{{
|
||||
row.hospitalName || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>采集专员</label><span>{{ row.collectorUserName || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>样本采集</label><span>{{ row.sampleCollectSummary || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>采集时间</label><span>{{ row.sampleCollectTime || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="protocol-card__block protocol-card__block--newborn">
|
||||
<div class="protocol-card__block-title">新生儿</div>
|
||||
<div class="protocol-card__kv-grid">
|
||||
<div class="protocol-card__kv">
|
||||
<label>姓名</label><span>{{ row.newbornName || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>性别</label
|
||||
><span>{{ newbornSexLabel(String(row.newbornSex || '')) || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>预产期</label><span>{{ row.expectedDeliveryDate || '—' }}</span>
|
||||
</div>
|
||||
<div class="protocol-card__kv">
|
||||
<label>分娩时间</label><span>{{ row.newbornDeliveryTime || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="protocol-card__block protocol-card__block--samples">
|
||||
<div class="protocol-card__block-title">
|
||||
样本信息
|
||||
<span v-if="getSampleItems(row).length" class="protocol-card__block-count">
|
||||
{{ getSampleItems(row).length }} 项
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="getSampleItems(row).length" class="protocol-card__sample-list">
|
||||
<div
|
||||
v-for="(item, sIdx) in getSampleItems(row)"
|
||||
:key="sIdx"
|
||||
class="protocol-card__sample-item"
|
||||
>
|
||||
<span class="protocol-card__sample-seq">{{ sIdx + 1 }}</span>
|
||||
<div class="protocol-card__sample-main">
|
||||
<div class="protocol-card__sample-line">
|
||||
<span class="protocol-card__sample-label">类型</span>
|
||||
<span class="protocol-card__sample-value">{{
|
||||
item.sampleTypeName || '—'
|
||||
}}</span>
|
||||
<el-tag
|
||||
v-if="item.sampleStatus"
|
||||
:type="sampleStatusTagType(item.sampleStatus)"
|
||||
size="small"
|
||||
class="protocol-card__sample-status"
|
||||
>
|
||||
{{ sampleStatusLabel(item.sampleStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="protocol-card__sample-line">
|
||||
<span class="protocol-card__sample-label">编码</span>
|
||||
<span class="protocol-card__sample-value protocol-card__sample-value--code">
|
||||
{{ item.sampleCode || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="protocol-card__sample-line">
|
||||
<span class="protocol-card__sample-label">运输</span>
|
||||
<span class="protocol-card__sample-value">{{
|
||||
formatTransportSummary(item.transport)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="protocol-card__empty">暂无样本</div>
|
||||
</section>
|
||||
|
||||
<section v-if="getPaymentSummary(row)" class="protocol-card__block protocol-card__block--payment">
|
||||
<div class="protocol-card__block-title">缴费信息</div>
|
||||
<QcWorkOrderProtocolPaymentPanel
|
||||
:summary="getPaymentSummary(row)"
|
||||
compact
|
||||
embedded
|
||||
hide-overall-pay-status
|
||||
hide-installment-detail-amounts
|
||||
/>
|
||||
</section>
|
||||
<section v-else class="protocol-card__block protocol-card__block--payment">
|
||||
<div class="protocol-card__block-title">缴费信息</div>
|
||||
<div class="protocol-card__empty">暂无缴费信息</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="protocol-card__foot">
|
||||
<div class="protocol-card__actions">
|
||||
<el-button class="btn-op-item" @click.stop="openAuditDialog(row)">
|
||||
<el-icon><CircleCheck /></el-icon>审核
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click.stop="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<el-empty
|
||||
v-else-if="!loading"
|
||||
class="protocol-card-empty"
|
||||
description="暂无协议单"
|
||||
:image-size="80"
|
||||
/>
|
||||
</div>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
@@ -386,8 +347,7 @@
|
||||
Delete,
|
||||
Search,
|
||||
RefreshLeft,
|
||||
CircleCheck,
|
||||
More
|
||||
CircleCheck
|
||||
} from '@element-plus/icons-vue'
|
||||
import ProtocolFormDialog from '../protocol/ProtocolFormDialog.vue'
|
||||
import ProtocolViewDialog from '../protocol/ProtocolViewDialog.vue'
|
||||
@@ -398,6 +358,8 @@
|
||||
type StatusChangeAction,
|
||||
type StatusChangeTarget
|
||||
} from '../protocol/ProtocolStatusChangeDialog.vue'
|
||||
import QcWorkOrderProtocolPaymentPanel from '@/views/quality-inspection/qc-work-order/QcWorkOrderProtocolPaymentPanel.vue'
|
||||
import type { QcProtocolPaymentSummary } from '@/api/qc-work-order'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import {
|
||||
fetchCollectionProtocolList,
|
||||
@@ -412,11 +374,100 @@
|
||||
protocolStatusLabel,
|
||||
protocolStatusTagType,
|
||||
newbornSexLabel,
|
||||
deliveryModeLabel
|
||||
sampleStatusLabel,
|
||||
sampleStatusTagType,
|
||||
transportTypeLabel
|
||||
} from '@/utils/status-tag-registry'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
|
||||
defineOptions({ name: 'CellStorageProtocolAuditTab' })
|
||||
|
||||
type ProtocolSampleTransport = {
|
||||
transportType?: string
|
||||
destSiteName?: string
|
||||
expressCompanyName?: string
|
||||
waybillNo?: string
|
||||
carrierName?: string
|
||||
carrierPhone?: string
|
||||
etaTime?: string
|
||||
}
|
||||
|
||||
type ProtocolSampleItem = {
|
||||
sampleTypeName: string
|
||||
sampleCode: string
|
||||
sampleStatus: string
|
||||
labStorageCode: string
|
||||
transport?: ProtocolSampleTransport | null
|
||||
}
|
||||
|
||||
function getPaymentSummary(row: CollectionProtocolRow): QcProtocolPaymentSummary | null {
|
||||
const summary = row.paymentSummary
|
||||
if (!summary || typeof summary !== 'object') return null
|
||||
return summary as QcProtocolPaymentSummary
|
||||
}
|
||||
|
||||
function formatTransportSummary(transport?: ProtocolSampleTransport | null): string {
|
||||
if (!transport?.transportType) return '—'
|
||||
const parts: string[] = [transportTypeLabel(String(transport.transportType))]
|
||||
const dest = String(transport.destSiteName || '').trim()
|
||||
if (dest) parts.push(dest)
|
||||
if (transport.transportType === 'kdys') {
|
||||
const expressName = String(transport.expressCompanyName || '').trim()
|
||||
if (expressName) parts.push(expressName)
|
||||
const waybill = String(transport.waybillNo || '').trim()
|
||||
if (waybill) parts.push(waybill)
|
||||
} else if (transport.transportType === 'ryxd') {
|
||||
const carrier = String(transport.carrierName || '').trim()
|
||||
if (carrier) parts.push(carrier)
|
||||
const phone = String(transport.carrierPhone || '').trim()
|
||||
if (phone) parts.push(phone)
|
||||
}
|
||||
const eta = String(transport.etaTime || '').trim()
|
||||
if (eta) parts.push(`预计 ${eta}`)
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
function getSampleItems(row: CollectionProtocolRow): ProtocolSampleItem[] {
|
||||
const rawItems = row.sampleItems
|
||||
if (Array.isArray(rawItems) && rawItems.length) {
|
||||
return rawItems.map((item) => {
|
||||
const rec = item as Record<string, unknown>
|
||||
const transportRaw = rec.transport
|
||||
const transport =
|
||||
transportRaw && typeof transportRaw === 'object'
|
||||
? (transportRaw as ProtocolSampleTransport)
|
||||
: null
|
||||
return {
|
||||
sampleTypeName: String(rec.sampleTypeName || '').trim(),
|
||||
sampleCode: String(rec.sampleCode || '').trim(),
|
||||
sampleStatus: String(rec.sampleStatus || '').trim(),
|
||||
labStorageCode: String(rec.labStorageCode || '').trim(),
|
||||
transport
|
||||
}
|
||||
})
|
||||
}
|
||||
const types = String(row.sampleTypeNames || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
const codes = String(row.sampleCodes || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
const labCodes = String(row.labStorageCodes || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
const len = Math.max(types.length, codes.length)
|
||||
if (!len) return []
|
||||
return Array.from({ length: len }, (_, i) => ({
|
||||
sampleTypeName: types[i] || '',
|
||||
sampleCode: codes[i] || '',
|
||||
sampleStatus: '',
|
||||
labStorageCode: labCodes[i] || '',
|
||||
transport: null
|
||||
}))
|
||||
}
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
@@ -443,8 +494,15 @@
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
const handleSelectionChange = (rows: CollectionProtocolRow[]) => {
|
||||
selectedRows.value = rows
|
||||
const isRowSelected = (id: string) => selectedRows.value.some((r) => String(r.id) === id)
|
||||
|
||||
const toggleRowSelection = (row: CollectionProtocolRow, checked: boolean) => {
|
||||
const id = String(row.id)
|
||||
if (checked) {
|
||||
if (!isRowSelected(id)) selectedRows.value = [...selectedRows.value, row]
|
||||
return
|
||||
}
|
||||
selectedRows.value = selectedRows.value.filter((r) => String(r.id) !== id)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
@@ -462,6 +520,8 @@
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
const validIds = new Set(tableData.value.map((r) => String(r.id)))
|
||||
selectedRows.value = selectedRows.value.filter((r) => validIds.has(String(r.id)))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -565,14 +625,6 @@
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleDropdownCommand(command: string, row: any) {
|
||||
switch (command) {
|
||||
case 'view': handleView(row); break
|
||||
case 'edit': handleEdit(row); break
|
||||
case 'delete': handleDelete(row); break
|
||||
}
|
||||
}
|
||||
|
||||
/* ====== 删除 ====== */
|
||||
function handleDelete(row: CollectionProtocolRow) {
|
||||
ElMessageBox.confirm(formatProtocolDocDeleteConfirm('cell_storage', row), '删除确认', {
|
||||
@@ -622,3 +674,359 @@
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.protocol-card-section {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.protocol-card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.protocol-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 12px 14px 12px 40px;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #c5e0d4;
|
||||
box-shadow: 0 2px 8px rgb(15 23 42 / 5%);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--primary, #10b981);
|
||||
box-shadow: 0 0 0 1px rgb(16 185 129 / 25%);
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card__select {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.protocol-card__aside {
|
||||
flex: 0 0 148px;
|
||||
min-width: 132px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-right: 12px;
|
||||
border-right: 1px solid #eef2f6;
|
||||
}
|
||||
|
||||
.protocol-card__status {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.protocol-card__create-time {
|
||||
margin-top: auto;
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.protocol-card__name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.protocol-card__index {
|
||||
flex-shrink: 0;
|
||||
min-width: 22px;
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
border-radius: 6px;
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
color: var(--primary-dark, #059669);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.protocol-card__name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e293b);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.protocol-card__contact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.protocol-card__sub {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&--id {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card__sections {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.protocol-card__block {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #eef2f6;
|
||||
|
||||
&--basic {
|
||||
flex: 1.1;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
&--newborn {
|
||||
flex: 0.85;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
&--samples {
|
||||
flex: 1.35;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
&--payment {
|
||||
flex: 1.2;
|
||||
min-width: 200px;
|
||||
padding-bottom: 4px;
|
||||
|
||||
:deep(.qc-protocol-payment--compact) {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.qc-protocol-payment__compact-details) {
|
||||
border-top-color: #e2e8f0;
|
||||
}
|
||||
|
||||
:deep(.qc-protocol-payment__detail-chip) {
|
||||
background: #fff;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card__block-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark, #059669);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.protocol-card__block-count {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.protocol-card__kv-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.protocol-card__kv {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
|
||||
label {
|
||||
flex-shrink: 0;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
span {
|
||||
min-width: 0;
|
||||
color: var(--text, #1e293b);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card__empty {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.protocol-card__sample-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.protocol-card__sample-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.protocol-card__sample-seq {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 4px;
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
color: var(--primary-dark, #059669);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.protocol-card__sample-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.protocol-card__sample-line {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.protocol-card__sample-label {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.protocol-card__sample-value {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
color: var(--text, #1e293b);
|
||||
word-break: break-all;
|
||||
|
||||
&--code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card__sample-status {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.protocol-card__foot {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding-left: 12px;
|
||||
border-left: 1px solid #eef2f6;
|
||||
}
|
||||
|
||||
.protocol-card__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
align-items: stretch;
|
||||
gap: 6px;
|
||||
min-width: 72px;
|
||||
|
||||
:deep(.el-button) {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-card-empty {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.protocol-card {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.protocol-card__sections {
|
||||
flex-basis: 100%;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.protocol-card__foot {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-left: none;
|
||||
border-top: 1px solid #eef2f6;
|
||||
padding-left: 0;
|
||||
padding-top: 10px;
|
||||
margin-left: 0;
|
||||
order: 4;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.protocol-card__sections {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.protocol-card__block {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,30 +2,30 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div
|
||||
class="panel-entity-switch panel-entity-switch--multi"
|
||||
role="tablist"
|
||||
aria-label="视图类型"
|
||||
>
|
||||
<button
|
||||
v-for="item in entityTabs"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="entity-switch-card"
|
||||
:class="{ 'is-active': activeTab === item.key }"
|
||||
role="tab"
|
||||
:aria-selected="activeTab === item.key"
|
||||
@click="activeTab = item.key"
|
||||
>
|
||||
<span class="entity-switch-card__icon"
|
||||
><el-icon><component :is="item.icon" /></el-icon
|
||||
></span>
|
||||
<span class="entity-switch-card__main">
|
||||
<span class="entity-switch-card__title">{{ item.title }}</span>
|
||||
<span class="entity-switch-card__desc">{{ item.desc }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="pill-tab-nav" role="tablist" aria-label="视图类型">
|
||||
<div class="pill-tab-nav__rail">
|
||||
<div class="pill-tab-nav__progress" :style="pillProgressStyle" />
|
||||
<button
|
||||
v-for="(item, index) in entityTabs"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="pill-tab-nav__step"
|
||||
:class="{
|
||||
'is-active': activeTab === item.key,
|
||||
'is-done': index < activeTabIndex
|
||||
}"
|
||||
role="tab"
|
||||
:aria-selected="activeTab === item.key"
|
||||
@click="activeTab = item.key"
|
||||
>
|
||||
<span class="pill-tab-nav__index">{{ index + 1 }}</span>
|
||||
<span class="pill-tab-nav__text">
|
||||
<span class="pill-tab-nav__title">{{ item.title }}</span>
|
||||
<span class="pill-tab-nav__desc">{{ item.desc }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<ProtocolAuditTab v-if="activeTab === 'protocol'" />
|
||||
<PaymentInfoTab v-else-if="activeTab === 'payment-info'" />
|
||||
@@ -36,8 +36,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
import { Document, Wallet, Medal, Calendar } from '@element-plus/icons-vue'
|
||||
import ProtocolAuditTab from './ProtocolAuditTab.vue'
|
||||
import PaymentInfoTab from './PaymentInfoTab.vue'
|
||||
import StorageCertTab from './StorageCertTab.vue'
|
||||
@@ -55,31 +53,26 @@
|
||||
key: EntityTabKey
|
||||
title: string
|
||||
desc: string
|
||||
icon: Component
|
||||
}> = [
|
||||
{
|
||||
key: 'protocol',
|
||||
title: '协议单管理',
|
||||
desc: '协议审核与状态维护',
|
||||
icon: Document
|
||||
desc: '协议审核与状态维护'
|
||||
},
|
||||
{
|
||||
key: 'payment-info',
|
||||
title: '缴费信息',
|
||||
desc: '缴费计划与到账查询',
|
||||
icon: Wallet
|
||||
desc: '缴费计划与到账查询'
|
||||
},
|
||||
{
|
||||
key: 'storage-cert',
|
||||
title: '储存证管理',
|
||||
desc: '储存证开具与管理',
|
||||
icon: Medal
|
||||
desc: '储存证开具与管理'
|
||||
},
|
||||
{
|
||||
key: 'fullmoon-todo',
|
||||
title: '满月待办管理',
|
||||
desc: '满月保险待办办理',
|
||||
icon: Calendar
|
||||
desc: '满月保险待办办理'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -87,6 +80,18 @@
|
||||
const router = useRouter()
|
||||
const activeTab = ref<EntityTabKey>('protocol')
|
||||
|
||||
const activeTabIndex = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
entityTabs.findIndex((item) => item.key === activeTab.value)
|
||||
)
|
||||
)
|
||||
|
||||
const pillProgressStyle = computed(() => ({
|
||||
width: `calc(${100 / entityTabs.length}% - 3px)`,
|
||||
transform: `translateX(${activeTabIndex.value * 100}%)`
|
||||
}))
|
||||
|
||||
onMounted(() => {
|
||||
const tab = String(route.query.tab || '')
|
||||
if (LEGACY_SAMPLE_TABS.includes(tab as (typeof LEGACY_SAMPLE_TABS)[number])) {
|
||||
@@ -103,110 +108,139 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.panel-entity-switch {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 16px 16px 12px;
|
||||
}
|
||||
|
||||
.panel-entity-switch--multi {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.entity-switch-card {
|
||||
flex: 0 1 168px;
|
||||
min-width: 152px;
|
||||
max-width: 180px;
|
||||
padding: 10px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.entity-switch-card__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.entity-switch-card__title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.entity-switch-card__desc {
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
}
|
||||
|
||||
.entity-switch-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 220px;
|
||||
max-width: 280px;
|
||||
flex: 1;
|
||||
padding: 14px 18px;
|
||||
border: 1px solid var(--mint-border, #c5e0d4);
|
||||
border-radius: 10px;
|
||||
background: var(--surface, #fff);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
|
||||
&:hover:not(.is-active) {
|
||||
border-color: #a8d4c4;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: var(--primary, #047857);
|
||||
background: var(--primary-bg, #e8f4ee);
|
||||
box-shadow: 0 0 0 1px rgb(4 120 87 / 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.entity-switch-card__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 20px;
|
||||
|
||||
.entity-switch-card.is-active & {
|
||||
background: #fff;
|
||||
color: var(--primary-dark, #065f46);
|
||||
}
|
||||
}
|
||||
|
||||
.entity-switch-card__main {
|
||||
.pill-tab-nav {
|
||||
padding: 16px 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entity-switch-card__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e293b);
|
||||
line-height: 1.4;
|
||||
.pill-tab-nav__rail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 70%;
|
||||
max-width: 100%;
|
||||
margin-inline: auto;
|
||||
background: var(--surface, #fff);
|
||||
border: 1px solid var(--border, #e2e8f0);
|
||||
border-radius: 10px;
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.entity-switch-card.is-active & {
|
||||
color: var(--primary-dark, #065f46);
|
||||
.pill-tab-nav__progress {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
left: 6px;
|
||||
background: var(--primary, #10b981);
|
||||
border: 1px solid var(--primary-dark, #059669);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgb(16 185 129 / 28%);
|
||||
transition:
|
||||
transform 0.25s ease,
|
||||
width 0.25s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.pill-tab-nav__step {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.pill-tab-nav__index {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pill-tab-nav__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pill-tab-nav__title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pill-tab-nav__desc {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pill-tab-nav__step.is-active .pill-tab-nav__index {
|
||||
background: #fff;
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
|
||||
.pill-tab-nav__step.is-active .pill-tab-nav__title,
|
||||
.pill-tab-nav__step.is-active .pill-tab-nav__desc {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pill-tab-nav__step.is-done .pill-tab-nav__index {
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
color: var(--primary-dark, #059669);
|
||||
border: 1px solid var(--mint-border, #c5e0d4);
|
||||
}
|
||||
|
||||
.pill-tab-nav__step.is-done .pill-tab-nav__title {
|
||||
color: var(--text, #1e293b);
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.pill-tab-nav__desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pill-tab-nav__text {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.entity-switch-card__desc {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
@media (width <= 720px) {
|
||||
.pill-tab-nav {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pill-tab-nav__rail {
|
||||
width: 100%;
|
||||
min-width: 680px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,291 @@
|
||||
// 采集协议单 · 缴费信息五列看板
|
||||
|
||||
.payment-kanban-board {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(260px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.payment-kanban-col {
|
||||
min-width: 260px;
|
||||
background: var(--table-head, #f8fafc);
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 320px);
|
||||
max-height: calc(100vh - 200px);
|
||||
|
||||
&--dfj {
|
||||
border-top: 3px solid #fbbf24;
|
||||
}
|
||||
|
||||
&--yjf {
|
||||
border-top: 3px solid var(--primary, #10b981);
|
||||
}
|
||||
|
||||
&--ytf {
|
||||
border-top: 3px solid #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-kanban-col-hd {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--mint-border, #d9e8e2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
background: var(--surface, #fff);
|
||||
border-radius: 10px 10px 0 0;
|
||||
|
||||
.payment-kanban-col--dfj & {
|
||||
background: linear-gradient(180deg, #fffbeb 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.payment-kanban-col--yjf & {
|
||||
background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.payment-kanban-col--ytf & {
|
||||
background: linear-gradient(180deg, #fef2f2 0%, #fff 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.payment-kanban-col-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #1e293b);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.payment-kanban-count {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface, #fff);
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
}
|
||||
|
||||
.payment-kanban-list {
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.payment-kanban-col-footer {
|
||||
padding: 8px 10px 10px;
|
||||
border-top: 1px solid var(--mint-border, #d9e8e2);
|
||||
flex-shrink: 0;
|
||||
background: var(--surface, #fff);
|
||||
border-radius: 0 0 10px 10px;
|
||||
|
||||
:deep(.el-pagination) {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
row-gap: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-pagination .el-select) {
|
||||
width: 88px;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-card {
|
||||
position: relative;
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
border-radius: 10px;
|
||||
background: var(--surface, #fff);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: rgb(16 185 129 / 45%);
|
||||
box-shadow: 0 4px 12px rgb(16 185 129 / 8%);
|
||||
}
|
||||
|
||||
&--due-soon {
|
||||
background: #fee2e2;
|
||||
|
||||
&:hover {
|
||||
background: #fecaca;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.payment-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px 8px;
|
||||
border-bottom: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.payment-card__head-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.payment-card__identity {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.payment-card__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #1e293b);
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.payment-card__sub {
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
word-break: break-all;
|
||||
|
||||
&--id {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-card__status {
|
||||
flex-shrink: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.payment-card__body {
|
||||
padding: 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.payment-card__kv {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
min-width: 0;
|
||||
|
||||
label {
|
||||
flex-shrink: 0;
|
||||
width: 72px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: var(--text, #1e293b);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&--amount span {
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
}
|
||||
|
||||
.payment-card__expand {
|
||||
padding: 0 12px 8px;
|
||||
}
|
||||
|
||||
.payment-card__expand-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
cursor: pointer;
|
||||
padding: 4px 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
}
|
||||
|
||||
.payment-card__expand-body {
|
||||
margin-top: 6px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--mint-border, #e2e8f0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.payment-card__foot {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 8px 12px 10px;
|
||||
border-top: 1px solid var(--mint-border, #e2e8f0);
|
||||
|
||||
:deep(.btn-op-item.el-button) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
height: 28px;
|
||||
font-size: 11px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-kanban-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 24px 12px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #94a3b8);
|
||||
border: 1px dashed var(--mint-border, #d9e8e2);
|
||||
border-radius: 10px;
|
||||
background: rgb(255 255 255 / 60%);
|
||||
}
|
||||
|
||||
@media (width <= 1024px) {
|
||||
.payment-kanban-board {
|
||||
grid-template-columns: 1fr;
|
||||
overflow-x: visible;
|
||||
}
|
||||
|
||||
.payment-kanban-col {
|
||||
min-width: 0;
|
||||
max-height: none;
|
||||
min-height: 280px;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
width="560px"
|
||||
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)"
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="1380px"
|
||||
width="1600px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
@@ -82,12 +84,11 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-grid form-grid--col2 basic-remark-attach-row">
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-grid form-grid--attachment">
|
||||
<el-form-item label="协议附件">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
@@ -169,7 +170,7 @@
|
||||
<div class="section-title">
|
||||
<div class="section-title-left"><h3>采集信息</h3></div>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-grid form-grid--col6">
|
||||
<el-form-item label="采集地区">
|
||||
<el-select
|
||||
v-model="formData.regionId"
|
||||
@@ -271,12 +272,22 @@
|
||||
<SampleTransportFormSection
|
||||
v-model:samples="formData.samples"
|
||||
:sample-type-options="sampleTypeOptions"
|
||||
:sample-type-options-loading="sampleTypeLoading"
|
||||
:sample-code-options="sampleCodeOptions"
|
||||
:sample-code-options-loading="sampleCodeLoading"
|
||||
:unit-options="unitOptions"
|
||||
:unit-options-loading="unitLoading"
|
||||
:site-options="siteOptions"
|
||||
:site-options-loading="siteLoading"
|
||||
:express-company-options="expressCompanyOptions"
|
||||
:express-company-options-loading="expressCompanyLoading"
|
||||
:show-price="true"
|
||||
@price-change="onSamplePriceChange"
|
||||
@search-sample-code="remoteSearchSampleCodes"
|
||||
@search-sample-type="remoteSearchSampleTypes"
|
||||
@search-unit="remoteSearchUnits"
|
||||
@search-site="remoteSearchSites"
|
||||
@search-express-company="remoteSearchExpressCompanies"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -486,7 +497,7 @@
|
||||
<!-- 缴费主表 -->
|
||||
<div class="payment-main-card">
|
||||
<div class="section-subtitle">缴费主表</div>
|
||||
<div class="form-grid form-grid--col5">
|
||||
<div class="form-grid form-grid--col6">
|
||||
<el-form-item label="缴费模式">
|
||||
<el-radio-group v-model="formData.payMode" @change="onPayModeChange">
|
||||
<el-radio label="dj">趸交</el-radio>
|
||||
@@ -547,68 +558,52 @@
|
||||
@change="handleFqParamsChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div class="installment-advance-ratios-row form-col-span-2">
|
||||
<el-form-item label="应缴费提前(天)" class="installment-advance-field">
|
||||
<el-input
|
||||
v-model="formData.installmentAdvanceDays"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="365"
|
||||
step="1"
|
||||
style="width: 120px"
|
||||
@change="handleFqParamsChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="缴费比例(%)" class="installment-ratios-field">
|
||||
<div class="installment-ratios-panel">
|
||||
<div class="installment-ratios-bar" aria-hidden="true">
|
||||
<div
|
||||
v-for="(ratio, rIdx) in formData.installmentRatios"
|
||||
:key="'bar-' + rIdx"
|
||||
class="installment-ratios-bar-seg"
|
||||
:class="{ 'is-auto': rIdx === formData.installmentRatios.length - 1 }"
|
||||
:style="{ flex: Math.max(Number(ratio) || 0, 0.001) }"
|
||||
:title="`第${rIdx + 1}期 ${ratio}%`"
|
||||
/>
|
||||
</div>
|
||||
<div class="installment-ratios-cells">
|
||||
<div
|
||||
v-for="(_ratio, rIdx) in formData.installmentRatios"
|
||||
:key="rIdx"
|
||||
class="installment-ratio-cell"
|
||||
:class="{ 'is-auto': rIdx === formData.installmentRatios.length - 1 }"
|
||||
>
|
||||
<span class="installment-ratio-cell-no">{{ rIdx + 1 }}期</span>
|
||||
<el-input
|
||||
v-model="formData.installmentRatios[rIdx]"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="100"
|
||||
step="any"
|
||||
:disabled="rIdx === formData.installmentRatios.length - 1"
|
||||
@change="onInstallmentRatioChange(rIdx)"
|
||||
>
|
||||
<template #suffix>%</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="installment-ratios-foot">
|
||||
<span class="form-label-tip">末期为自动补齐</span>
|
||||
<span
|
||||
class="installment-ratios-sum"
|
||||
:class="{ 'is-error': !!installmentRatiosError }"
|
||||
>
|
||||
合计 {{ installmentRatiosSum }}%
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="installmentRatiosError" class="form-tip form-tip--error">
|
||||
{{ installmentRatiosError }}
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="应缴费提前(天)">
|
||||
<el-input
|
||||
v-model="formData.installmentAdvanceDays"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="365"
|
||||
step="1"
|
||||
style="width: 100%"
|
||||
@change="handleFqParamsChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="formData.payMode === 'fq'" class="installment-amounts-row">
|
||||
<el-form-item label="分期金额(元)" class="installment-amounts-field">
|
||||
<div class="installment-amounts-cells">
|
||||
<div
|
||||
v-for="(_amount, rIdx) in formData.installmentRatios"
|
||||
:key="rIdx"
|
||||
class="installment-amount-cell"
|
||||
>
|
||||
<span class="installment-amount-cell-no">{{ rIdx + 1 }}期</span>
|
||||
<AmountInput
|
||||
v-model="formData.installmentRatios[rIdx]"
|
||||
:min="0"
|
||||
placeholder="金额"
|
||||
@change="onInstallmentAmountChange(rIdx)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<div class="installment-amounts-summary-row">
|
||||
<span
|
||||
class="installment-amounts-sum"
|
||||
:class="{ 'is-error': !!installmentAmountsError }"
|
||||
>
|
||||
合计 {{ fmtMoney(installmentAmountsSum) }}
|
||||
<template v-if="formData.contractAmount != null">
|
||||
/ 合同 {{ fmtMoney(formData.contractAmount) }}
|
||||
</template>
|
||||
</span>
|
||||
<div v-if="installmentAmountsError" class="form-tip form-tip--error">
|
||||
{{ installmentAmountsError }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 缴费明细 -->
|
||||
@@ -844,7 +839,14 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- ========== 文件预览弹窗 ========== -->
|
||||
<el-dialog v-model="previewVisible" title="文件预览" width="800px" align-center destroy-on-close>
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="文件预览"
|
||||
width="800px"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="previewType === 'image'" class="preview-container">
|
||||
<img
|
||||
:src="previewUrl"
|
||||
@@ -900,16 +902,14 @@
|
||||
import { fetchEthnicList, type EthnicOption } from '@/api/ethnic'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
calcInstallmentDueAmountsByRatios,
|
||||
calcUnpaidAmountsByRatios,
|
||||
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
DEFAULT_INSTALLMENT_RATIOS_6,
|
||||
defaultInstallmentRatios,
|
||||
finalizeInstallmentRatios,
|
||||
isInstallmentRatiosValid,
|
||||
resizeInstallmentRatios,
|
||||
syncFqPaymentDetailRowCount
|
||||
} from '@/utils/protocol-payment-installment'
|
||||
DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
isInstallmentAmountsValid,
|
||||
normalizeInstallmentAmounts,
|
||||
resizeInstallmentAmounts,
|
||||
scaleInstallmentAmountsToContract,
|
||||
sumInstallmentAmounts,
|
||||
syncFqPaymentDetailRowCount
|
||||
} from '@/utils/protocol-payment-installment'
|
||||
import {
|
||||
applyPayModeSwitch,
|
||||
canAddPaymentDetail,
|
||||
@@ -955,7 +955,7 @@
|
||||
installmentIntervalMonths: null,
|
||||
installmentStartDate: '',
|
||||
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
installmentRatios: [...DEFAULT_INSTALLMENT_RATIOS_6]
|
||||
installmentRatios: [] as number[]
|
||||
})
|
||||
|
||||
const customerId = ref<string | null>(null)
|
||||
@@ -997,6 +997,10 @@
|
||||
const unitOptions = ref<Array<{ id: string; unitName: string; unitCode: string }>>([])
|
||||
const sampleCodeOptions = ref<Api.BizSampleCode.SampleCodeItem[]>([])
|
||||
const sampleCodeLoading = ref(false)
|
||||
const sampleTypeLoading = ref(false)
|
||||
const unitLoading = ref(false)
|
||||
const siteLoading = ref(false)
|
||||
const expressCompanyLoading = ref(false)
|
||||
const expressCompanyOptions = ref<Api.ExpressCompany.ExpressCompanyItem[]>([])
|
||||
const siteOptions = ref<Api.Site.SiteItem[]>([])
|
||||
let sampleRowSeq = 0
|
||||
@@ -1194,7 +1198,7 @@
|
||||
lumpSumDeadline: '',
|
||||
installmentStartDate: '',
|
||||
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS as number | null,
|
||||
installmentRatios: [...DEFAULT_INSTALLMENT_RATIOS_6] as number[],
|
||||
installmentRatios: [] as number[] as number[],
|
||||
// Payment Details
|
||||
paymentDetails: [] as Array<Record<string, any>>
|
||||
})
|
||||
@@ -1248,7 +1252,7 @@
|
||||
lumpSumDeadline: '',
|
||||
installmentStartDate: '',
|
||||
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
installmentRatios: [...DEFAULT_INSTALLMENT_RATIOS_6],
|
||||
installmentRatios: [] as number[],
|
||||
paymentDetails: []
|
||||
})
|
||||
customerId.value = null
|
||||
@@ -1262,6 +1266,11 @@
|
||||
salesSnapshot.value = null
|
||||
regionOptions.value = []
|
||||
hospitalOptions.value = []
|
||||
sampleTypeOptions.value = []
|
||||
unitOptions.value = []
|
||||
sampleCodeOptions.value = []
|
||||
siteOptions.value = []
|
||||
expressCompanyOptions.value = []
|
||||
activeFormTab.value = 'basic'
|
||||
lastConfirmedPayMode.value = 'dj'
|
||||
lastConfirmedFqParams.value = {
|
||||
@@ -1269,7 +1278,7 @@
|
||||
installmentIntervalMonths: null,
|
||||
installmentStartDate: '',
|
||||
installmentAdvanceDays: DEFAULT_INSTALLMENT_ADVANCE_DAYS,
|
||||
installmentRatios: [...DEFAULT_INSTALLMENT_RATIOS_6]
|
||||
installmentRatios: [] as number[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1377,13 +1386,11 @@
|
||||
formData.installmentAdvanceDays = DEFAULT_INSTALLMENT_ADVANCE_DAYS
|
||||
}
|
||||
const count = toIntOrUndefined(formData.installmentCount) || 6
|
||||
if (reset || !formData.installmentRatios?.length) {
|
||||
formData.installmentRatios = defaultInstallmentRatios(count)
|
||||
} else {
|
||||
formData.installmentRatios = finalizeInstallmentRatios(
|
||||
resizeInstallmentRatios(formData.installmentRatios, count)
|
||||
)
|
||||
}
|
||||
formData.installmentRatios = normalizeInstallmentAmounts(
|
||||
formData.installmentRatios || [],
|
||||
formData.contractAmount,
|
||||
count
|
||||
)
|
||||
}
|
||||
|
||||
function toIntOrUndefined(val: unknown): number | undefined {
|
||||
@@ -1428,7 +1435,7 @@
|
||||
installmentIntervalMonths: toFloatOrUndefined(formData.installmentIntervalMonths) ?? null,
|
||||
installmentStartDate: formData.installmentStartDate || '',
|
||||
installmentAdvanceDays: toIntOrUndefined(formData.installmentAdvanceDays) ?? null,
|
||||
installmentRatios: finalizeInstallmentRatios([...(formData.installmentRatios || [])])
|
||||
installmentRatios: [...(formData.installmentRatios || [])].map((v) => Number(v || 0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1584,7 +1591,6 @@
|
||||
resetForm()
|
||||
formData.motherEthnic = DEFAULT_MOTHER_ETHNIC
|
||||
formData.registerDate = new Date().toISOString().slice(0, 10).replace(/-/g, '')
|
||||
loadSampleOptions()
|
||||
void loadEthnicOptions()
|
||||
void remoteSearchCustomers('')
|
||||
|
||||
@@ -1686,12 +1692,18 @@
|
||||
formData.installmentStartDate = String(pay.installmentStartDate || '')
|
||||
formData.installmentAdvanceDays =
|
||||
pay.installmentAdvanceDays ?? DEFAULT_INSTALLMENT_ADVANCE_DAYS
|
||||
const loadedRatios = Array.isArray(pay.installmentRatios) ? pay.installmentRatios : []
|
||||
if (loadedRatios.length) {
|
||||
formData.installmentRatios = loadedRatios.map((v: unknown) => Number(v))
|
||||
const loadedAmounts = Array.isArray(pay.installmentRatios) ? pay.installmentRatios : []
|
||||
if (loadedAmounts.length) {
|
||||
formData.installmentRatios = loadedAmounts.map((v: unknown) => Number(v))
|
||||
}
|
||||
if (formData.payMode === 'fq') {
|
||||
applyFqPaymentDefaults()
|
||||
const count = toIntOrUndefined(formData.installmentCount) || 6
|
||||
formData.installmentRatios = normalizeInstallmentAmounts(
|
||||
formData.installmentRatios,
|
||||
formData.contractAmount,
|
||||
count
|
||||
)
|
||||
syncLastConfirmedFqParams()
|
||||
}
|
||||
}
|
||||
@@ -1741,8 +1753,7 @@
|
||||
dialogFormLoading.value = true
|
||||
|
||||
try {
|
||||
const [, , bundle] = await Promise.all([
|
||||
loadSampleOptions(),
|
||||
const [, bundle] = await Promise.all([
|
||||
loadEthnicOptions(),
|
||||
fetchCollectionProtocolDetail(String(row.id)).catch(() => null)
|
||||
])
|
||||
@@ -1779,7 +1790,13 @@
|
||||
}
|
||||
await Promise.all(hydrateTasks)
|
||||
if (formData.samples.length) {
|
||||
ensureSampleCodesInOptions(formData.samples)
|
||||
await Promise.all([
|
||||
remoteSearchSampleTypes(''),
|
||||
remoteSearchSites(''),
|
||||
remoteSearchUnits(''),
|
||||
remoteSearchSampleCodes('')
|
||||
])
|
||||
hydrateSampleLookupOptions()
|
||||
}
|
||||
} catch {
|
||||
/* 保留列表行已填基础字段 */
|
||||
@@ -1792,13 +1809,13 @@
|
||||
if (!formRef.value) return
|
||||
await formRef.value.validate()
|
||||
if (formData.payMode === 'fq') {
|
||||
if (installmentRatiosError.value) {
|
||||
await ElMessageBox.alert(installmentRatiosError.value, '缴费比例无效', { type: 'warning' })
|
||||
if (installmentAmountsError.value) {
|
||||
await ElMessageBox.alert(installmentAmountsError.value, '分期金额无效', { type: 'warning' })
|
||||
return
|
||||
}
|
||||
const ratios = finalizeInstallmentRatios([...(formData.installmentRatios || [])])
|
||||
if (!isInstallmentRatiosValid(ratios)) {
|
||||
await ElMessageBox.alert('缴费比例之和须等于 100%', '缴费比例无效', { type: 'warning' })
|
||||
const amounts = (formData.installmentRatios || []).map((v) => Number(v || 0))
|
||||
if (!isInstallmentAmountsValid(amounts, formData.contractAmount)) {
|
||||
await ElMessageBox.alert('分期金额合计须等于合同金额', '分期金额无效', { type: 'warning' })
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1883,7 +1900,7 @@
|
||||
installmentAdvanceDays: toIntOrUndefined(formData.installmentAdvanceDays),
|
||||
installmentRatios:
|
||||
formData.payMode === 'fq'
|
||||
? finalizeInstallmentRatios([...(formData.installmentRatios || [])])
|
||||
? (formData.installmentRatios || []).map((v) => Number(v || 0))
|
||||
: undefined
|
||||
},
|
||||
paymentDetails: formData.paymentDetails.map(
|
||||
@@ -2116,6 +2133,89 @@
|
||||
sampleCodeOptions.value = merged
|
||||
}
|
||||
|
||||
function ensureSampleTypesInOptions(samples: Array<Record<string, any>>) {
|
||||
const merged = [...sampleTypeOptions.value]
|
||||
for (const s of samples) {
|
||||
const id = String(s.sampleTypeId || '')
|
||||
if (!id) continue
|
||||
const name = String(s.sampleTypeName || '').trim()
|
||||
const existing = merged.find((x) => x.id === id)
|
||||
if (existing) {
|
||||
if (name && (!existing.typeName || existing.typeName === existing.id)) {
|
||||
existing.typeName = name
|
||||
}
|
||||
continue
|
||||
}
|
||||
merged.push({
|
||||
id,
|
||||
typeCode: '',
|
||||
typeName: name || id,
|
||||
storageYears: s.storageYears ?? null,
|
||||
storagePrice: s.originalPrice ?? null
|
||||
})
|
||||
}
|
||||
sampleTypeOptions.value = merged
|
||||
}
|
||||
|
||||
function ensureUnitsInOptions(samples: Array<Record<string, any>>) {
|
||||
const merged = [...unitOptions.value]
|
||||
for (const s of samples) {
|
||||
const id = String(s.unitId || '')
|
||||
if (!id) continue
|
||||
const name = String(s.unitName || '').trim()
|
||||
const existing = merged.find((x) => x.id === id)
|
||||
if (existing) {
|
||||
if (name && (!existing.unitName || existing.unitName === existing.id)) {
|
||||
existing.unitName = name
|
||||
}
|
||||
continue
|
||||
}
|
||||
merged.push({ id, unitName: name || id, unitCode: String(s.unitCode || '') })
|
||||
}
|
||||
unitOptions.value = merged
|
||||
}
|
||||
|
||||
function ensureSitesInOptions(samples: Array<Record<string, any>>) {
|
||||
const merged = [...siteOptions.value]
|
||||
for (const s of samples) {
|
||||
const id = String(s.transport?.destSiteId || '')
|
||||
if (!id) continue
|
||||
const name = String(s.transport?.destSiteName || '').trim()
|
||||
const existing = merged.find((x) => x.id === id)
|
||||
if (existing) {
|
||||
if (name && (!existing.siteName || existing.siteName === existing.id)) {
|
||||
existing.siteName = name
|
||||
}
|
||||
continue
|
||||
}
|
||||
merged.push({
|
||||
id,
|
||||
siteCode: '',
|
||||
siteName: name || id,
|
||||
siteType: '',
|
||||
status: '',
|
||||
address: '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
businessScope: '',
|
||||
siteImage: '',
|
||||
tenantNo: '',
|
||||
orderNum: 0,
|
||||
remark: '',
|
||||
createTime: '',
|
||||
updateTime: ''
|
||||
})
|
||||
}
|
||||
siteOptions.value = merged
|
||||
}
|
||||
|
||||
function hydrateSampleLookupOptions() {
|
||||
ensureSampleTypesInOptions(formData.samples)
|
||||
ensureSampleCodesInOptions(formData.samples)
|
||||
ensureUnitsInOptions(formData.samples)
|
||||
ensureSitesInOptions(formData.samples)
|
||||
}
|
||||
|
||||
/* ====== 客户检索(母亲信息) ====== */
|
||||
function formatCustomerLabel(c: { customerName: string; phoneNo: string; idCardNo: string }) {
|
||||
const parts = [c.customerName || '']
|
||||
@@ -2248,23 +2348,91 @@
|
||||
formData.salesUserId = u.id
|
||||
}
|
||||
|
||||
/* ====== 样本类型/单位选项 ====== */
|
||||
async function loadSampleOptions() {
|
||||
async function remoteSearchSampleCodes(query: string) {
|
||||
sampleCodeLoading.value = true
|
||||
try {
|
||||
const [stRes, uRes, scRes, ecRes, siteRes] = await Promise.all([
|
||||
fetchCellSampleTypeList({ page: 1, pageSize: 100 }),
|
||||
fetchItemUnitList({ page: 1, pageSize: 100 }),
|
||||
fetchSampleCodeList({ page: 1, pageSize: 100 }),
|
||||
fetchExpressCompanyList({ page: 1, pageSize: 100 }),
|
||||
fetchSiteList({ page: 1, pageSize: 100 })
|
||||
])
|
||||
sampleTypeOptions.value = (stRes.list || []) as any
|
||||
unitOptions.value = (uRes.list || []) as any
|
||||
sampleCodeOptions.value = (scRes.list || []) as any
|
||||
expressCompanyOptions.value = ecRes.list || []
|
||||
siteOptions.value = siteRes.list || []
|
||||
const res = await fetchSampleCodeList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
sampleCodeOptions.value = (res.list || []) as Api.BizSampleCode.SampleCodeItem[]
|
||||
ensureSampleCodesInOptions(formData.samples)
|
||||
} catch {
|
||||
/* ignore */
|
||||
sampleCodeOptions.value = []
|
||||
ensureSampleCodesInOptions(formData.samples)
|
||||
} finally {
|
||||
sampleCodeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remoteSearchSampleTypes(query: string) {
|
||||
sampleTypeLoading.value = true
|
||||
try {
|
||||
const res = await fetchCellSampleTypeList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
sampleTypeOptions.value = (res.list || []) as any
|
||||
ensureSampleTypesInOptions(formData.samples)
|
||||
} catch {
|
||||
sampleTypeOptions.value = []
|
||||
ensureSampleTypesInOptions(formData.samples)
|
||||
} finally {
|
||||
sampleTypeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remoteSearchUnits(query: string) {
|
||||
unitLoading.value = true
|
||||
try {
|
||||
const res = await fetchItemUnitList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
unitOptions.value = (res.list || []) as any
|
||||
ensureUnitsInOptions(formData.samples)
|
||||
} catch {
|
||||
unitOptions.value = []
|
||||
ensureUnitsInOptions(formData.samples)
|
||||
} finally {
|
||||
unitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remoteSearchSites(query: string) {
|
||||
siteLoading.value = true
|
||||
try {
|
||||
const res = await fetchSiteList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
siteOptions.value = res.list || []
|
||||
ensureSitesInOptions(formData.samples)
|
||||
} catch {
|
||||
siteOptions.value = []
|
||||
ensureSitesInOptions(formData.samples)
|
||||
} finally {
|
||||
siteLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remoteSearchExpressCompanies(query: string) {
|
||||
expressCompanyLoading.value = true
|
||||
try {
|
||||
const res = await fetchExpressCompanyList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
expressCompanyOptions.value = res.list || []
|
||||
} catch {
|
||||
expressCompanyOptions.value = []
|
||||
} finally {
|
||||
expressCompanyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2341,33 +2509,32 @@
|
||||
function onInstallmentCountChange() {
|
||||
const count = toIntOrUndefined(formData.installmentCount) || 0
|
||||
if (count > 0) {
|
||||
formData.installmentRatios = finalizeInstallmentRatios(
|
||||
resizeInstallmentRatios(formData.installmentRatios || [], count)
|
||||
formData.installmentRatios = resizeInstallmentAmounts(
|
||||
formData.installmentRatios || [],
|
||||
count,
|
||||
formData.contractAmount
|
||||
)
|
||||
}
|
||||
handleFqParamsChange()
|
||||
}
|
||||
|
||||
function onInstallmentRatioChange(changedIdx: number) {
|
||||
if (changedIdx === formData.installmentRatios.length - 1) return
|
||||
formData.installmentRatios = finalizeInstallmentRatios([...formData.installmentRatios])
|
||||
function onInstallmentAmountChange(_changedIdx: number) {
|
||||
syncPaymentDueAmounts()
|
||||
handleFqParamsChange()
|
||||
}
|
||||
|
||||
const installmentRatiosError = computed(() => {
|
||||
const ratios = formData.installmentRatios || []
|
||||
if (!ratios.length) return ''
|
||||
const head = ratios.slice(0, -1)
|
||||
const sum = head.reduce((s, v) => s + Number(v || 0), 0)
|
||||
if (sum > 100 + 0.001) return '前 N−1 期比例之和不能超过 100%'
|
||||
const installmentAmountsError = computed(() => {
|
||||
const amounts = formData.installmentRatios || []
|
||||
if (!amounts.length || formData.contractAmount == null) return ''
|
||||
if (amounts.some((v) => Number(v) < 0)) return '分期金额不能为负数'
|
||||
const sum = sumInstallmentAmounts(amounts)
|
||||
if (Math.abs(sum - Number(formData.contractAmount)) > 0.01) {
|
||||
return `分期金额合计须等于合同金额(${fmtMoney(formData.contractAmount)})`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const installmentRatiosSum = computed(() => {
|
||||
const total = (formData.installmentRatios || []).reduce((s, v) => s + Number(v || 0), 0)
|
||||
return Math.round(total * 100) / 100
|
||||
})
|
||||
const installmentAmountsSum = computed(() => sumInstallmentAmounts(formData.installmentRatios || []))
|
||||
|
||||
/** 按缴费模式同步各明细应缴费金额(跳过已锁定明细) */
|
||||
function syncPaymentDueAmounts() {
|
||||
@@ -2388,27 +2555,16 @@
|
||||
if (formData.payMode === 'fq') {
|
||||
const count = formData.installmentCount || formData.paymentDetails.length
|
||||
if (!count) return
|
||||
const ratios = finalizeInstallmentRatios(formData.installmentRatios || [])
|
||||
const amounts = normalizeInstallmentAmounts(
|
||||
formData.installmentRatios || [],
|
||||
contract,
|
||||
Number(count)
|
||||
)
|
||||
const paidRows = formData.paymentDetails.filter((d) => String(d.payStatus) === 'yjf')
|
||||
let amounts: number[]
|
||||
if (paidRows.length > 0 && formData.paymentDetails.some((d) => isPayDetailLocked(d))) {
|
||||
const paidSum = sumPaidAmount(formData.paymentDetails)
|
||||
const remaining = Math.round((Number(contract) - paidSum) * 100) / 100
|
||||
const unpaidIndices = Array.from({ length: Number(count) }, (_, i) => i).filter(
|
||||
(i) => !paidRows.some((d) => Number(d.installmentNo) === i + 1)
|
||||
)
|
||||
const unpaidAmounts = calcUnpaidAmountsByRatios(remaining, ratios, unpaidIndices)
|
||||
amounts = Array(Number(count)).fill(0)
|
||||
unpaidIndices.forEach((idx, j) => {
|
||||
amounts[idx] = unpaidAmounts[j] ?? 0
|
||||
})
|
||||
paidRows.forEach((d) => {
|
||||
const no = Number(d.installmentNo)
|
||||
if (no >= 1 && no <= Number(count)) amounts[no - 1] = Number(d.dueAmount) || 0
|
||||
})
|
||||
} else {
|
||||
amounts = calcInstallmentDueAmountsByRatios(Number(contract), ratios)
|
||||
}
|
||||
paidRows.forEach((d) => {
|
||||
const no = Number(d.installmentNo)
|
||||
if (no >= 1 && no <= Number(count)) amounts[no - 1] = Number(d.dueAmount) || 0
|
||||
})
|
||||
formData.paymentDetails.forEach((d, i) => {
|
||||
if (isPayDetailLocked(d)) return
|
||||
if (amounts[i] !== undefined) d.dueAmount = amounts[i]
|
||||
@@ -2422,7 +2578,14 @@
|
||||
if (s.sampleStatus === 'yzf') return sum
|
||||
return sum + (Number(s.price) || 0)
|
||||
}, 0)
|
||||
formData.contractAmount = Math.round(total * 100) / 100
|
||||
const nextContract = Math.round(total * 100) / 100
|
||||
if (formData.payMode === 'fq' && (formData.installmentRatios || []).length) {
|
||||
formData.installmentRatios = scaleInstallmentAmountsToContract(
|
||||
formData.installmentRatios,
|
||||
nextContract
|
||||
)
|
||||
}
|
||||
formData.contractAmount = nextContract
|
||||
syncPaymentDueAmounts()
|
||||
}
|
||||
|
||||
@@ -2756,15 +2919,20 @@
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
&--col6 {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
&--attachment {
|
||||
margin-top: 16px;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.basic-remark-attach-row {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.form-col-span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
@@ -3143,13 +3311,10 @@
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
&--col5 {
|
||||
&--col5,
|
||||
&--col6 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
&--col2.basic-remark-attach-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.client-sub-cards {
|
||||
@@ -3157,106 +3322,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
.installment-advance-ratios-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
.installment-amounts-row {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.installment-advance-field {
|
||||
flex: 0 0 auto;
|
||||
.installment-amounts-field.el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.installment-ratios-field {
|
||||
flex: 1 1 360px;
|
||||
min-width: 0;
|
||||
.installment-amounts-summary-row {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.installment-ratios-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #c5e0d4;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.installment-ratios-bar {
|
||||
display: flex;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.installment-ratios-bar-seg {
|
||||
background: #047857;
|
||||
min-width: 2px;
|
||||
transition: flex 0.2s ease;
|
||||
|
||||
&.is-auto {
|
||||
background: #6bb99a;
|
||||
}
|
||||
|
||||
& + & {
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.installment-ratios-cells {
|
||||
.installment-amounts-cells {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 12px 16px;
|
||||
}
|
||||
|
||||
.installment-ratio-cell {
|
||||
.installment-amount-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1 1 72px;
|
||||
min-width: 72px;
|
||||
max-width: 96px;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
padding-left: 8px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&.is-auto :deep(.el-input__wrapper) {
|
||||
background: #e8f4ee;
|
||||
}
|
||||
gap: 6px;
|
||||
min-width: 120px;
|
||||
flex: 1 1 120px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.installment-ratio-cell-no {
|
||||
font-size: 11px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.installment-ratios-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.installment-ratios-sum {
|
||||
.installment-amount-cell-no {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.installment-amounts-sum {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
|
||||
&.is-error {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tip--error {
|
||||
margin-top: 6px;
|
||||
color: #dc2626;
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
width="560px"
|
||||
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)"
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
width="1600px"
|
||||
class="dialog-form-layout protocol-view-dialog"
|
||||
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)"
|
||||
@@ -31,43 +33,6 @@
|
||||
|
||||
<template v-if="bundle">
|
||||
<div>
|
||||
<!-- KPI 摘要条 -->
|
||||
<div class="kpi-strip">
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-card__icon" aria-hidden="true">
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-card__main">
|
||||
<div class="kpi-label">协议状态</div>
|
||||
<div class="kpi-value">
|
||||
<el-tag
|
||||
:type="protocolStatusTagType(String(protocol?.protocolStatus || ''))"
|
||||
size="small"
|
||||
>
|
||||
{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-card__icon" aria-hidden="true">
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-card__main">
|
||||
<div class="kpi-label">母亲姓名</div>
|
||||
<div class="kpi-value">{{ dash(partyCustomer?.motherName) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-card">
|
||||
<div class="kpi-card__icon" aria-hidden="true">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-card__main">
|
||||
<div class="kpi-label">登记日期</div>
|
||||
<div class="kpi-value">{{ formatYmd(protocol?.registerDate) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 基本信息 -->
|
||||
<div id="view-section-basic" class="section-card section-card--white">
|
||||
<div class="section-title"><h3>基本信息</h3></div>
|
||||
@@ -769,9 +734,9 @@
|
||||
}}</div></div
|
||||
>
|
||||
<div class="payment-metric"
|
||||
><div class="pm-label">缴费比例</div
|
||||
><div class="pm-label">分期金额(元)</div
|
||||
><div class="pm-value">{{
|
||||
formatInstallmentRatios(payment?.installmentRatios)
|
||||
formatInstallmentAmounts(payment?.installmentRatios)
|
||||
}}</div></div
|
||||
>
|
||||
</template>
|
||||
@@ -1152,7 +1117,14 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="previewVisible" title="文件预览" width="800px" align-center destroy-on-close>
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="文件预览"
|
||||
width="800px"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="previewType === 'image'" class="preview-container">
|
||||
<img
|
||||
:src="previewUrl"
|
||||
@@ -1172,7 +1144,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { Calendar, CircleCheck, Close, User } from '@element-plus/icons-vue'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import ProtocolAttachmentReadonly from './ProtocolAttachmentReadonly.vue'
|
||||
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
|
||||
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
|
||||
@@ -1467,9 +1439,9 @@
|
||||
return dash(val)
|
||||
}
|
||||
|
||||
function formatInstallmentRatios(ratios?: number[] | null) {
|
||||
if (!ratios?.length) return '—'
|
||||
return ratios.map((r) => `${r}%`).join(' / ')
|
||||
function formatInstallmentAmounts(values?: number[] | null) {
|
||||
if (!values?.length) return '—'
|
||||
return values.map((v) => fmtMoney(v)).join(' / ')
|
||||
}
|
||||
|
||||
function formatYm(val: unknown) {
|
||||
|
||||
@@ -2,30 +2,30 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div
|
||||
class="panel-entity-switch panel-entity-switch--multi"
|
||||
role="tablist"
|
||||
aria-label="视图类型"
|
||||
>
|
||||
<button
|
||||
v-for="item in entityTabs"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="entity-switch-card"
|
||||
:class="{ 'is-active': activeTab === item.key }"
|
||||
role="tab"
|
||||
:aria-selected="activeTab === item.key"
|
||||
@click="activeTab = item.key"
|
||||
>
|
||||
<span class="entity-switch-card__icon"
|
||||
><el-icon><component :is="item.icon" /></el-icon
|
||||
></span>
|
||||
<span class="entity-switch-card__main">
|
||||
<span class="entity-switch-card__title">{{ item.title }}</span>
|
||||
<span class="entity-switch-card__desc">{{ item.desc }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="pill-tab-nav" role="tablist" aria-label="视图类型">
|
||||
<div class="pill-tab-nav__rail">
|
||||
<div class="pill-tab-nav__progress" :style="pillProgressStyle" />
|
||||
<button
|
||||
v-for="(item, index) in entityTabs"
|
||||
:key="item.key"
|
||||
type="button"
|
||||
class="pill-tab-nav__step"
|
||||
:class="{
|
||||
'is-active': activeTab === item.key,
|
||||
'is-done': index < activeTabIndex
|
||||
}"
|
||||
role="tab"
|
||||
:aria-selected="activeTab === item.key"
|
||||
@click="activeTab = item.key"
|
||||
>
|
||||
<span class="pill-tab-nav__index">{{ index + 1 }}</span>
|
||||
<span class="pill-tab-nav__text">
|
||||
<span class="pill-tab-nav__title">{{ item.title }}</span>
|
||||
<span class="pill-tab-nav__desc">{{ item.desc }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<SampleCollectionInfoTab v-if="activeTab === 'sample-collection'" />
|
||||
<SampleReceptionInfoTab v-else-if="activeTab === 'sample-reception'" />
|
||||
@@ -36,8 +36,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
import { Box, DocumentChecked, FolderOpened, FirstAidKit } from '@element-plus/icons-vue'
|
||||
import SampleCollectionInfoTab from '../protocol-audit/SampleCollectionInfoTab.vue'
|
||||
import SampleReceptionInfoTab from '../protocol-audit/SampleReceptionInfoTab.vue'
|
||||
import SampleQcInfoTab from '../protocol-audit/SampleQcInfoTab.vue'
|
||||
@@ -58,37 +56,44 @@
|
||||
key: EntityTabKey
|
||||
title: string
|
||||
desc: string
|
||||
icon: Component
|
||||
}> = [
|
||||
{
|
||||
key: 'sample-collection',
|
||||
title: '样本采集信息',
|
||||
desc: '协议样本采集进度',
|
||||
icon: FirstAidKit
|
||||
desc: '协议样本采集进度'
|
||||
},
|
||||
{
|
||||
key: 'sample-reception',
|
||||
title: '样本接收信息',
|
||||
desc: '接收结论与短信通知',
|
||||
icon: Box
|
||||
desc: '接收结论与短信通知'
|
||||
},
|
||||
{
|
||||
key: 'sample-qc',
|
||||
title: '样本质检信息',
|
||||
desc: '质检结论与短信通知',
|
||||
icon: DocumentChecked
|
||||
desc: '质检结论与短信通知'
|
||||
},
|
||||
{
|
||||
key: 'storage-entry',
|
||||
title: '样本入库信息',
|
||||
desc: '样本入库记录查询',
|
||||
icon: FolderOpened
|
||||
desc: '样本入库记录查询'
|
||||
}
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
const activeTab = ref<EntityTabKey>('sample-collection')
|
||||
|
||||
const activeTabIndex = computed(() =>
|
||||
Math.max(
|
||||
0,
|
||||
entityTabs.findIndex((item) => item.key === activeTab.value)
|
||||
)
|
||||
)
|
||||
|
||||
const pillProgressStyle = computed(() => ({
|
||||
width: `calc(${100 / entityTabs.length}% - 3px)`,
|
||||
transform: `translateX(${activeTabIndex.value * 100}%)`
|
||||
}))
|
||||
|
||||
onMounted(() => {
|
||||
const tab = String(route.query.tab || '')
|
||||
if (VALID_TABS.includes(tab as EntityTabKey)) {
|
||||
@@ -98,106 +103,139 @@
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.panel-entity-switch {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
padding: 16px 16px 12px;
|
||||
}
|
||||
|
||||
.panel-entity-switch--multi {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
.entity-switch-card {
|
||||
flex: 0 1 168px;
|
||||
min-width: 152px;
|
||||
max-width: 180px;
|
||||
padding: 10px 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.entity-switch-card__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.entity-switch-card__title {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.entity-switch-card__desc {
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.entity-switch-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 18px;
|
||||
border: 1px solid var(--mint-border, #c5e0d4);
|
||||
border-radius: 10px;
|
||||
background: var(--surface, #fff);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
background-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
|
||||
&:hover:not(.is-active) {
|
||||
border-color: #a8d4c4;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: var(--primary, #047857);
|
||||
background: var(--primary-bg, #e8f4ee);
|
||||
box-shadow: 0 0 0 1px rgb(4 120 87 / 12%);
|
||||
}
|
||||
}
|
||||
|
||||
.entity-switch-card__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 20px;
|
||||
|
||||
.entity-switch-card.is-active & {
|
||||
background: #fff;
|
||||
color: var(--primary-dark, #065f46);
|
||||
}
|
||||
}
|
||||
|
||||
.entity-switch-card__main {
|
||||
.pill-tab-nav {
|
||||
padding: 16px 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.entity-switch-card__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e293b);
|
||||
line-height: 1.4;
|
||||
.pill-tab-nav__rail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 70%;
|
||||
max-width: 100%;
|
||||
margin-inline: auto;
|
||||
background: var(--surface, #fff);
|
||||
border: 1px solid var(--border, #e2e8f0);
|
||||
border-radius: 10px;
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.entity-switch-card.is-active & {
|
||||
color: var(--primary-dark, #065f46);
|
||||
.pill-tab-nav__progress {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
left: 6px;
|
||||
background: var(--primary, #10b981);
|
||||
border: 1px solid var(--primary-dark, #059669);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgb(16 185 129 / 28%);
|
||||
transition:
|
||||
transform 0.25s ease,
|
||||
width 0.25s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.pill-tab-nav__step {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 44px;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.pill-tab-nav__index {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pill-tab-nav__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pill-tab-nav__title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pill-tab-nav__desc {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.pill-tab-nav__step.is-active .pill-tab-nav__index {
|
||||
background: #fff;
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
|
||||
.pill-tab-nav__step.is-active .pill-tab-nav__title,
|
||||
.pill-tab-nav__step.is-active .pill-tab-nav__desc {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pill-tab-nav__step.is-done .pill-tab-nav__index {
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
color: var(--primary-dark, #059669);
|
||||
border: 1px solid var(--mint-border, #c5e0d4);
|
||||
}
|
||||
|
||||
.pill-tab-nav__step.is-done .pill-tab-nav__title {
|
||||
color: var(--text, #1e293b);
|
||||
}
|
||||
|
||||
@media (width <= 960px) {
|
||||
.pill-tab-nav__desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pill-tab-nav__text {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.entity-switch-card__desc {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
@media (width <= 720px) {
|
||||
.pill-tab-nav {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pill-tab-nav__rail {
|
||||
width: 100%;
|
||||
min-width: 680px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user