提交
This commit is contained in:
1 parent
a9667f00cc
commit
817dfff43c
174 files changed
+17383
-14508
No files matched your search
@@ -30,7 +30,7 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">支持模糊查询;默认展示近 30 天内创建的客户。</span>
|
||||
<span class="filter-hint">支持模糊查询。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset"
|
||||
><el-icon><RefreshLeft /></el-icon>重置</el-button
|
||||
@@ -274,22 +274,8 @@
|
||||
|
||||
defineOptions({ name: 'BizCustomer' })
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultCreateTimeRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const createTimeRange = ref<[string, string] | null>(getDefaultCreateTimeRange())
|
||||
const createTimeRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<Api.BizCustomer.CustomerItem[]>([])
|
||||
const selectedRows = ref<Api.BizCustomer.CustomerItem[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -371,7 +357,7 @@
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
createTimeRange.value = getDefaultCreateTimeRange()
|
||||
createTimeRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -417,6 +417,7 @@
|
||||
if (sourceType === 'manual_in') return 'success'
|
||||
if (sourceType === 'manual_out') return 'warning'
|
||||
if (sourceType === 'manual_discard') return 'danger'
|
||||
if (sourceType === 'outbound_handover') return 'primary'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
@@ -424,6 +425,7 @@
|
||||
if (sourceType === 'manual_in') return '手动入库'
|
||||
if (sourceType === 'manual_out') return '手动出库'
|
||||
if (sourceType === 'manual_discard') return '手动废弃'
|
||||
if (sourceType === 'outbound_handover') return '出库交接'
|
||||
return sourceType || '-'
|
||||
}
|
||||
|
||||
|
||||
@@ -486,37 +486,17 @@
|
||||
const emit = defineEmits<{ actionDone: [] }>()
|
||||
|
||||
const filterHint =
|
||||
'支持客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示应缴费时间前 15 天至后 15 天内的明细。'
|
||||
'支持客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索。'
|
||||
|
||||
function formatPaymentAmount(v: unknown): string {
|
||||
if (v == null || v === '') return '-'
|
||||
return '¥' + formatMoneyDisplay(v)
|
||||
}
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultInstallmentDueDateRange(): [string, string] {
|
||||
const today = new Date()
|
||||
const start = new Date(today)
|
||||
const end = new Date(today)
|
||||
start.setDate(start.getDate() - 15)
|
||||
end.setDate(end.getDate() + 15)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const pendingSearchForm = reactive({ keyword: '' })
|
||||
const confirmedSearchForm = reactive({ keyword: '' })
|
||||
const pendingInstallmentDueDateRange = ref<[string, string] | null>(
|
||||
getDefaultInstallmentDueDateRange()
|
||||
)
|
||||
const confirmedInstallmentDueDateRange = ref<[string, string] | null>(
|
||||
getDefaultInstallmentDueDateRange()
|
||||
)
|
||||
const pendingInstallmentDueDateRange = ref<[string, string] | null>(null)
|
||||
const confirmedInstallmentDueDateRange = ref<[string, string] | null>(null)
|
||||
|
||||
const pendingData = ref<Record<string, unknown>[]>([])
|
||||
const confirmedData = ref<Record<string, unknown>[]>([])
|
||||
@@ -605,7 +585,7 @@
|
||||
|
||||
function handlePendingReset() {
|
||||
pendingSearchForm.keyword = ''
|
||||
pendingInstallmentDueDateRange.value = getDefaultInstallmentDueDateRange()
|
||||
pendingInstallmentDueDateRange.value = null
|
||||
pendingPagination.page = 1
|
||||
loadPending()
|
||||
}
|
||||
@@ -617,7 +597,7 @@
|
||||
|
||||
function handleConfirmedReset() {
|
||||
confirmedSearchForm.keyword = ''
|
||||
confirmedInstallmentDueDateRange.value = getDefaultInstallmentDueDateRange()
|
||||
confirmedInstallmentDueDateRange.value = null
|
||||
confirmedPagination.page = 1
|
||||
loadConfirmed()
|
||||
}
|
||||
|
||||
@@ -527,37 +527,17 @@
|
||||
}
|
||||
|
||||
const filterHint =
|
||||
'支持客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示应缴费时间前 15 天至后 15 天内的明细。'
|
||||
'支持客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索。'
|
||||
|
||||
function formatPaymentAmount(v: unknown): string {
|
||||
if (v == null || v === '') return '-'
|
||||
return '¥' + formatMoneyDisplay(v)
|
||||
}
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultInstallmentDueDateRange(): [string, string] {
|
||||
const today = new Date()
|
||||
const start = new Date(today)
|
||||
const end = new Date(today)
|
||||
start.setDate(start.getDate() - 15)
|
||||
end.setDate(end.getDate() + 15)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const pendingSearchForm = reactive({ keyword: '' })
|
||||
const confirmedSearchForm = reactive({ keyword: '' })
|
||||
const pendingInstallmentDueDateRange = ref<[string, string] | null>(
|
||||
getDefaultInstallmentDueDateRange()
|
||||
)
|
||||
const confirmedInstallmentDueDateRange = ref<[string, string] | null>(
|
||||
getDefaultInstallmentDueDateRange()
|
||||
)
|
||||
const pendingInstallmentDueDateRange = ref<[string, string] | null>(null)
|
||||
const confirmedInstallmentDueDateRange = ref<[string, string] | null>(null)
|
||||
|
||||
const pendingData = ref<Record<string, unknown>[]>([])
|
||||
const confirmedData = ref<Record<string, unknown>[]>([])
|
||||
@@ -659,7 +639,7 @@
|
||||
|
||||
function handlePendingReset() {
|
||||
pendingSearchForm.keyword = ''
|
||||
pendingInstallmentDueDateRange.value = getDefaultInstallmentDueDateRange()
|
||||
pendingInstallmentDueDateRange.value = null
|
||||
pendingPagination.page = 1
|
||||
loadPending()
|
||||
}
|
||||
@@ -671,7 +651,7 @@
|
||||
|
||||
function handleConfirmedReset() {
|
||||
confirmedSearchForm.keyword = ''
|
||||
confirmedInstallmentDueDateRange.value = getDefaultInstallmentDueDateRange()
|
||||
confirmedInstallmentDueDateRange.value = null
|
||||
confirmedPagination.page = 1
|
||||
loadConfirmed()
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>支持按姓名模糊检索;待办状态默认未办保险,满月日期默认近 30 天。</span
|
||||
>支持按姓名模糊检索;待办状态默认未办保险。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
@@ -477,25 +477,8 @@
|
||||
|
||||
defineOptions({ name: 'CellStorageFullmoonTodoTab' })
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultFullmoonDateRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 搜索 & 主表
|
||||
// ---------------------------------------------------------------------------
|
||||
const searchForm = reactive({ keyword: '', todoStatus: '0' })
|
||||
const fullmoonDateRange = ref<[string, string] | null>(getDefaultFullmoonDateRange())
|
||||
const fullmoonDateRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<any[]>([])
|
||||
const tableRef = ref()
|
||||
const selectedRows = ref<any[]>([])
|
||||
@@ -670,7 +653,7 @@
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.todoStatus = '0'
|
||||
fullmoonDateRange.value = getDefaultFullmoonDateRange()
|
||||
fullmoonDateRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>关键词、转账方式、应缴费时间对五列看板均生效;默认应缴费时间为当天前 10 天至后 20
|
||||
天。</span
|
||||
>关键词、转账方式、应缴费时间对五列看板均生效。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
@@ -389,23 +388,8 @@
|
||||
return { list: [], total: 0, page: 1, pageSize: 10, loading: false }
|
||||
}
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultInstallmentDueDateRange(): [string, string] {
|
||||
const start = new Date()
|
||||
const end = new Date()
|
||||
start.setDate(start.getDate() - 10)
|
||||
end.setDate(end.getDate() + 20)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const searchForm = reactive({ keyword: '', transferType: '' })
|
||||
const installmentDueDateRange = ref<[string, string] | null>(getDefaultInstallmentDueDateRange())
|
||||
const installmentDueDateRange = ref<[string, string] | null>(null)
|
||||
const columnStates = reactive<Record<PayStatusKey, KanbanColumnState>>({
|
||||
dfj: createColumnState(),
|
||||
jfqrz: createColumnState(),
|
||||
@@ -680,7 +664,7 @@
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.transferType = ''
|
||||
installmentDueDateRange.value = getDefaultInstallmentDueDateRange()
|
||||
installmentDueDateRange.value = null
|
||||
loadAllColumns(true)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span
|
||||
>列表为概要信息,详情见弹窗。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset"
|
||||
@@ -468,26 +468,12 @@
|
||||
}))
|
||||
}
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultCreateTimeRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
/* ====== 列表 ====== */
|
||||
const searchForm = reactive<{ keyword: string; protocolStatus: string }>({
|
||||
keyword: '',
|
||||
protocolStatus: ''
|
||||
})
|
||||
const createTimeRange = ref<[string, string] | null>(getDefaultCreateTimeRange())
|
||||
const createTimeRange = ref<[string, string] | null>(null)
|
||||
const sampleCollectTimeRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<CollectionProtocolRow[]>([])
|
||||
const selectedRows = ref<CollectionProtocolRow[]>([])
|
||||
@@ -535,7 +521,7 @@
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.protocolStatus = ''
|
||||
createTimeRange.value = getDefaultCreateTimeRange()
|
||||
createTimeRange.value = null
|
||||
sampleCollectTimeRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>展示全部协议样本(每个样本一条);支持按姓名、样本编码检索;默认展示近 30 天内创建的样本。</span
|
||||
>展示全部协议样本(每个样本一条);支持按姓名、样本编码检索。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
@@ -241,22 +241,8 @@
|
||||
return row.qcStatus === 'tg' || row.qcStatus === 'btg'
|
||||
}
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultSampleTimeRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const searchForm = reactive({ keyword: '', qcStatus: '', smsNotified: '' })
|
||||
const sampleTimeRange = ref<[string, string] | null>(getDefaultSampleTimeRange())
|
||||
const sampleTimeRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
@@ -292,7 +278,7 @@
|
||||
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { keyword: '', qcStatus: '', smsNotified: '' })
|
||||
sampleTimeRange.value = getDefaultSampleTimeRange()
|
||||
sampleTimeRange.value = null
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>支持按姓名、样本编码模糊检索;默认展示近 30 天内接收的样本。</span
|
||||
>支持按姓名、样本编码模糊检索。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
@@ -239,22 +239,8 @@
|
||||
|
||||
defineOptions({ name: 'CellStorageSampleReceptionInfoTab' })
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultReceptionTimeRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const searchForm = reactive({ keyword: '', acceptConclusion: '', smsNotified: '' })
|
||||
const receptionTimeRange = ref<[string, string] | null>(getDefaultReceptionTimeRange())
|
||||
const receptionTimeRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
@@ -290,7 +276,7 @@
|
||||
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { keyword: '', acceptConclusion: '', smsNotified: '' })
|
||||
receptionTimeRange.value = getDefaultReceptionTimeRange()
|
||||
receptionTimeRange.value = null
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>支持按姓名模糊检索;制作状态默认未制作,入库日期默认近 30 天。</span
|
||||
>支持按姓名模糊检索;制作状态默认未制作。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
@@ -380,25 +380,8 @@
|
||||
|
||||
defineOptions({ name: 'CellStorageStorageCertTab' })
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultStorageInDateRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 搜索 & 主表
|
||||
// ---------------------------------------------------------------------------
|
||||
const searchForm = reactive({ keyword: '', makeStatus: '0' })
|
||||
const storageInDateRange = ref<[string, string] | null>(getDefaultStorageInDateRange())
|
||||
const storageInDateRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
@@ -490,7 +473,7 @@
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.makeStatus = '0'
|
||||
storageInDateRange.value = getDefaultStorageInDateRange()
|
||||
storageInDateRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>支持按姓名、样本编码模糊检索;默认展示近 30 天内入库的样本。</span
|
||||
>支持按姓名、样本编码模糊检索。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
@@ -230,22 +230,8 @@
|
||||
|
||||
defineOptions({ name: 'CellStorageStorageEntryInfoTab' })
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultStorageTimeRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const searchForm = reactive({ keyword: '', storageStatus: '', smsNotified: '' })
|
||||
const storageTimeRange = ref<[string, string] | null>(getDefaultStorageTimeRange())
|
||||
const storageTimeRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
@@ -281,7 +267,7 @@
|
||||
|
||||
function handleReset() {
|
||||
Object.assign(searchForm, { keyword: '', storageStatus: '', smsNotified: '' })
|
||||
storageTimeRange.value = getDefaultStorageTimeRange()
|
||||
storageTimeRange.value = null
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
|
||||
@@ -106,141 +106,3 @@
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pill-tab-nav {
|
||||
padding: 16px 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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: var(--mint-text-muted, #334155);
|
||||
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: var(--mint-text-muted, #334155);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@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>
|
||||
@@ -2783,42 +2783,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Sticky Tab 导航(与查看弹窗 detail-tabs 一致) ---- */
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin: 0 28px 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
SECTION CARDS
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
@@ -1936,48 +1936,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Sticky Tab 导航 ---- */
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: default;
|
||||
opacity: 0.65;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- KPI 摘要条(统一薄荷绿通栏 + 图标徽章) ---- */
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
@@ -2104,16 +2062,12 @@
|
||||
&--banner {
|
||||
margin: -22px -24px 18px;
|
||||
padding: 10px 24px;
|
||||
border-bottom: none;
|
||||
border-bottom: 1px solid rgba(16, 185, 129, 0.12);
|
||||
border-radius: 12px 12px 0 0;
|
||||
background: var(--primary, #10b981);
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
|
||||
h3 {
|
||||
color: #fff;
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +56,7 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span
|
||||
>
|
||||
<span class="filter-hint">列表为概要信息,详情见弹窗。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset"
|
||||
><el-icon><RefreshLeft /></el-icon>重置</el-button
|
||||
@@ -316,26 +314,12 @@
|
||||
|
||||
defineOptions({ name: 'CellStorageProtocol' })
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultCreateTimeRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
/* ====== 列表 ====== */
|
||||
const searchForm = reactive<{ keyword: string; protocolStatus: string }>({
|
||||
keyword: '',
|
||||
protocolStatus: ''
|
||||
})
|
||||
const createTimeRange = ref<[string, string] | null>(getDefaultCreateTimeRange())
|
||||
const createTimeRange = ref<[string, string] | null>(null)
|
||||
const sampleCollectTimeRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<CollectionProtocolRow[]>([])
|
||||
const selectedRows = ref<CollectionProtocolRow[]>([])
|
||||
@@ -374,7 +358,7 @@
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.protocolStatus = ''
|
||||
createTimeRange.value = getDefaultCreateTimeRange()
|
||||
createTimeRange.value = null
|
||||
sampleCollectTimeRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
|
||||
@@ -101,141 +101,3 @@
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pill-tab-nav {
|
||||
padding: 16px 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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: var(--mint-text-muted, #334155);
|
||||
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: var(--mint-text-muted, #334155);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@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>
|
||||
@@ -31,7 +31,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>按预约编号、来访单位、联系人模糊查询;默认展示未来 30 天内的到访预约。</span
|
||||
>按预约编号、来访单位、联系人模糊查询。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset"
|
||||
@@ -288,22 +288,8 @@
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultVisitDateRange(): [string, string] {
|
||||
const start = new Date()
|
||||
const end = new Date()
|
||||
end.setDate(end.getDate() + 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const visitDateRange = ref<[string, string] | null>(getDefaultVisitDateRange())
|
||||
const visitDateRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<AppointmentRow[]>([])
|
||||
const selectedRows = ref<AppointmentRow[]>([])
|
||||
const loading = ref(false)
|
||||
@@ -417,7 +403,7 @@
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
visitDateRange.value = getDefaultVisitDateRange()
|
||||
visitDateRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">默认展示近 30 天内创建的流水。</span>
|
||||
<span class="filter-hint">支持按流水类型、状态等条件检索。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
@@ -644,27 +644,7 @@
|
||||
done: []
|
||||
}
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultLast30DaysRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const FINANCE_TABS = [
|
||||
{ name: 'pending', label: '待处理' },
|
||||
{ name: 'done', label: '已处理' }
|
||||
] as const
|
||||
|
||||
const activeTab = ref('pending')
|
||||
const createTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
|
||||
const createTimeRange = ref<[string, string] | null>(null)
|
||||
const searchForm = reactive({
|
||||
txnType: '' as string,
|
||||
txnStatus: ['dqr'] as string[],
|
||||
@@ -716,7 +696,7 @@
|
||||
searchForm.txnType = ''
|
||||
searchForm.txnStatus = [...TAB_STATUS_MAP[activeTab.value]]
|
||||
searchForm.keyword = ''
|
||||
createTimeRange.value = getDefaultLast30DaysRange()
|
||||
createTimeRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-if="rows.length"
|
||||
:data="rows"
|
||||
stripe
|
||||
class="list-table inner-table"
|
||||
class="list-table"
|
||||
:max-height="maxHeight"
|
||||
>
|
||||
<el-table-column
|
||||
@@ -27,6 +27,30 @@
|
||||
<span class="amount-text">¥{{ formatAmount(row.amount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="showBalance"
|
||||
label="变动前余额(元)"
|
||||
min-width="130"
|
||||
align="right"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.txnStatus === 'yqr'">¥{{ formatAmount(row.balanceBefore) }}</span>
|
||||
<span v-else class="void-info muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="showBalance"
|
||||
label="变动后余额(元)"
|
||||
min-width="130"
|
||||
align="right"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.txnStatus === 'yqr'">¥{{ formatAmount(row.balanceAfter) }}</span>
|
||||
<span v-else class="void-info muted">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="txnStatusTagType(row.txnStatus)" size="small">
|
||||
@@ -70,10 +94,12 @@
|
||||
defineProps<{
|
||||
rows: Record<string, any>[]
|
||||
maxHeight?: number | string
|
||||
showBalance?: boolean
|
||||
}>(),
|
||||
{
|
||||
rows: () => [],
|
||||
maxHeight: 360
|
||||
maxHeight: 360,
|
||||
showBalance: false
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">默认展示近 30 天内创建的订单;渠道名称输入后自动检索。</span>
|
||||
<span class="filter-hint">渠道名称输入后自动检索。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
@@ -1084,21 +1084,7 @@
|
||||
}
|
||||
|
||||
// ── 搜索 ──────────────────────────────────────────────────────────────
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultLast30DaysRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const createTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
|
||||
const createTimeRange = ref<[string, string] | null>(null)
|
||||
|
||||
const searchForm = reactive<{
|
||||
orderType: string
|
||||
@@ -1153,7 +1139,7 @@
|
||||
searchForm.orderType = ''
|
||||
searchForm.orderStatus = []
|
||||
searchForm.keyword = ''
|
||||
createTimeRange.value = getDefaultLast30DaysRange()
|
||||
createTimeRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -123,58 +123,74 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="view-section-invoice" class="section-card section-card--green">
|
||||
<div id="view-section-invoice" class="section-card section-card--white">
|
||||
<div class="section-title"><h3>开票信息</h3></div>
|
||||
<div v-if="invoiceList.length" class="sub-cards">
|
||||
<div v-for="(row, idx) in invoiceList" :key="row.id || idx" class="sub-card">
|
||||
<div class="sub-card-header">
|
||||
<h4>{{ row.companyName || '未命名' }}</h4>
|
||||
</div>
|
||||
<div class="data-grid data-grid--2">
|
||||
<div v-if="row.taxNo" class="data-item">
|
||||
<div class="data-label">税号</div>
|
||||
<div class="data-value">{{ dash(row.taxNo) }}</div>
|
||||
</div>
|
||||
<div v-if="row.phone || row.companyPhone" class="data-item">
|
||||
<div class="data-label">电话</div>
|
||||
<div class="data-value">{{ dash(row.phone || row.companyPhone) }}</div>
|
||||
</div>
|
||||
<div v-if="row.companyAddress" class="data-item data-item--full">
|
||||
<div class="data-label">单位地址</div>
|
||||
<div class="data-value data-value--muted">{{ dash(row.companyAddress) }}</div>
|
||||
</div>
|
||||
<div v-if="row.bankName" class="data-item">
|
||||
<div class="data-label">开户银行</div>
|
||||
<div class="data-value">{{ dash(row.bankName) }}</div>
|
||||
</div>
|
||||
<div v-if="row.bankAccount" class="data-item">
|
||||
<div class="data-label">银行账号</div>
|
||||
<div class="data-value">{{ dash(row.bankAccount) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="invoiceList.length" class="list-body list-wrap">
|
||||
<el-table
|
||||
:data="invoiceList"
|
||||
class="list-table"
|
||||
stripe
|
||||
row-key="id"
|
||||
max-height="360"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" />
|
||||
<el-table-column
|
||||
prop="companyName"
|
||||
label="单位名称"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="taxNo" label="税号" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
prop="companyAddress"
|
||||
label="单位地址"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="电话" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ dash(row.phone || row.companyPhone) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="bankName"
|
||||
label="开户银行"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="bankAccount"
|
||||
label="银行账号"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-empty v-else description="暂无开票信息" :image-size="48" />
|
||||
</div>
|
||||
|
||||
<div id="view-section-mail" class="section-card section-card--white">
|
||||
<div class="section-title"><h3>邮寄地址</h3></div>
|
||||
<div v-if="mailList.length" class="sub-cards">
|
||||
<div v-for="(row, idx) in mailList" :key="row.id || idx" class="sub-card">
|
||||
<div class="sub-card-header">
|
||||
<h4>{{ row.mailAddress || '未命名' }}</h4>
|
||||
</div>
|
||||
<div class="data-grid data-grid--2">
|
||||
<div v-if="row.mailContactName || row.mailContact" class="data-item">
|
||||
<div class="data-label">联系人</div>
|
||||
<div class="data-value">{{ dash(row.mailContactName || row.mailContact) }}</div>
|
||||
</div>
|
||||
<div v-if="row.mailContactPhone || row.mailPhone" class="data-item">
|
||||
<div class="data-label">联系电话</div>
|
||||
<div class="data-value">{{ dash(row.mailContactPhone || row.mailPhone) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="mailList.length" class="list-body list-wrap">
|
||||
<el-table :data="mailList" class="list-table" stripe row-key="id" max-height="360">
|
||||
<el-table-column type="index" label="序号" min-width="68" />
|
||||
<el-table-column
|
||||
prop="mailAddress"
|
||||
label="邮寄地址"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="联系人" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ dash(row.mailContactName || row.mailContact) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系电话" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ dash(row.mailContactPhone || row.mailPhone) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<el-empty v-else description="暂无邮寄地址" :image-size="48" />
|
||||
</div>
|
||||
@@ -187,70 +203,50 @@
|
||||
¥ {{ accountLoading ? '—' : formatAmount(accountData?.balanceAmount) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="account-quotas-head">
|
||||
<h4 class="account-quotas-title">产品份数</h4>
|
||||
<div class="account-subsection-head">
|
||||
<h4 class="account-subsection-title">账户流水</h4>
|
||||
</div>
|
||||
<div v-loading="accountTxnLoading" class="list-body list-wrap channel-view-txn-wrap">
|
||||
<ServiceOrderTxnTable :rows="accountTxnRows" max-height="320" show-balance />
|
||||
<div v-if="accountTxnTotal > accountTxnPageSize" class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="accountTxnPage"
|
||||
:page-size="accountTxnPageSize"
|
||||
:total="accountTxnTotal"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="loadAccountTxns"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="account-subsection-head account-subsection-head--spaced">
|
||||
<h4 class="account-subsection-title">产品份数</h4>
|
||||
</div>
|
||||
<div v-if="quotasLoading" v-loading="true" style="min-height: 120px"></div>
|
||||
<div
|
||||
v-else-if="productQuotas.length"
|
||||
class="list-body list-wrap account-quota-list-wrap"
|
||||
>
|
||||
<div v-else-if="productQuotas.length" class="list-body list-wrap">
|
||||
<el-table
|
||||
:data="productQuotas"
|
||||
class="list-table account-quota-table"
|
||||
class="list-table"
|
||||
stripe
|
||||
row-key="id"
|
||||
max-height="360"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column type="index" label="序号" min-width="68" />
|
||||
<el-table-column
|
||||
prop="projectName"
|
||||
label="项目名称"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="cellTypeName"
|
||||
label="细胞类型"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="specText"
|
||||
label="规格"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="totalQty"
|
||||
label="总份数"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="usedQty"
|
||||
label="已用份数"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="remainingQty"
|
||||
label="剩余份数"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="specText" label="规格" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="totalQty" label="总份数" min-width="100" align="right" />
|
||||
<el-table-column prop="usedQty" label="已用份数" min-width="100" align="right" />
|
||||
<el-table-column prop="remainingQty" label="剩余份数" min-width="100" align="right" />
|
||||
</el-table>
|
||||
</div>
|
||||
<el-empty v-else description="暂无产品份数数据" :image-size="48" />
|
||||
@@ -298,66 +294,41 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="(agreementPricesMap[agr.id] || []).length > 0"
|
||||
class="list-body list-wrap agreement-price-list-wrap"
|
||||
class="list-body list-wrap"
|
||||
>
|
||||
<el-table
|
||||
:data="agreementPricesMap[agr.id] || []"
|
||||
class="list-table agreement-price-table"
|
||||
class="list-table"
|
||||
stripe
|
||||
row-key="id"
|
||||
max-height="360"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column type="index" label="序号" min-width="68" />
|
||||
<el-table-column
|
||||
prop="projectName"
|
||||
label="项目名称"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="projectCode"
|
||||
label="项目编码"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="cellTypeName"
|
||||
label="细胞类型"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="specText"
|
||||
label="规格"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="原价(元)"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<el-table-column prop="specText" label="规格" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="原价(元)" min-width="100" align="right">
|
||||
<template #default="{ row: priceRow }">
|
||||
{{ formatPrice(priceRow.originalPrice) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="协议价(元)"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<el-table-column label="协议价(元)" min-width="100" align="right">
|
||||
<template #default="{ row: priceRow }">
|
||||
<span class="data-value--price">{{
|
||||
formatPrice(priceRow.agreementPrice)
|
||||
@@ -368,8 +339,6 @@
|
||||
prop="remark"
|
||||
label="备注"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
@@ -427,6 +396,7 @@
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { Calendar, Close, CollectionTag, OfficeBuilding, User } from '@element-plus/icons-vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import ServiceOrderTxnTable from '../channel-service-order/ServiceOrderTxnTable.vue'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import {
|
||||
@@ -445,7 +415,8 @@
|
||||
fetchChannelAgreements,
|
||||
fetchChannelAgreementPrices,
|
||||
fetchChannelAccount,
|
||||
fetchChannelProductQuotas
|
||||
fetchChannelProductQuotas,
|
||||
fetchServiceOrderTxnList
|
||||
} from '@/api/admin'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -477,6 +448,11 @@
|
||||
const accountLoading = ref(false)
|
||||
const quotasLoading = ref(false)
|
||||
const accountLoaded = ref(false)
|
||||
const accountTxnRows = ref<Record<string, any>[]>([])
|
||||
const accountTxnLoading = ref(false)
|
||||
const accountTxnPage = ref(1)
|
||||
const accountTxnPageSize = ref(10)
|
||||
const accountTxnTotal = ref(0)
|
||||
|
||||
const channel = computed(() => props.listRow || null)
|
||||
|
||||
@@ -559,25 +535,59 @@
|
||||
|
||||
const formatAmount = (v: unknown): string => formatMoneyDisplay(v as any, 2)
|
||||
|
||||
async function loadAccountTxns(page = 1) {
|
||||
if (!props.channelId) return
|
||||
accountTxnLoading.value = true
|
||||
try {
|
||||
const res = await fetchServiceOrderTxnList({
|
||||
channelId: props.channelId,
|
||||
page,
|
||||
pageSize: accountTxnPageSize.value
|
||||
})
|
||||
const data = (res as any)?.data ?? res
|
||||
accountTxnRows.value = Array.isArray(data?.list) ? data.list : []
|
||||
accountTxnTotal.value = Number(data?.total) || 0
|
||||
accountTxnPage.value = page
|
||||
} catch {
|
||||
accountTxnRows.value = []
|
||||
accountTxnTotal.value = 0
|
||||
} finally {
|
||||
accountTxnLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAccountData() {
|
||||
if (!props.channelId || accountLoaded.value) return
|
||||
accountLoading.value = true
|
||||
quotasLoading.value = true
|
||||
accountTxnLoading.value = true
|
||||
try {
|
||||
const [accRes, quotaRes] = await Promise.all([
|
||||
const [accRes, quotaRes, txnRes] = await Promise.all([
|
||||
fetchChannelAccount(props.channelId),
|
||||
fetchChannelProductQuotas(props.channelId)
|
||||
fetchChannelProductQuotas(props.channelId),
|
||||
fetchServiceOrderTxnList({
|
||||
channelId: props.channelId,
|
||||
page: 1,
|
||||
pageSize: accountTxnPageSize.value
|
||||
})
|
||||
])
|
||||
accountData.value = (accRes as any)?.data ?? accRes ?? null
|
||||
const quotaRows = Array.isArray(quotaRes) ? quotaRes : (quotaRes as any)?.list || []
|
||||
productQuotas.value = quotaRows
|
||||
const txnData = (txnRes as any)?.data ?? txnRes
|
||||
accountTxnRows.value = Array.isArray(txnData?.list) ? txnData.list : []
|
||||
accountTxnTotal.value = Number(txnData?.total) || 0
|
||||
accountTxnPage.value = 1
|
||||
accountLoaded.value = true
|
||||
} catch {
|
||||
accountData.value = null
|
||||
productQuotas.value = []
|
||||
accountTxnRows.value = []
|
||||
accountTxnTotal.value = 0
|
||||
} finally {
|
||||
accountLoading.value = false
|
||||
quotasLoading.value = false
|
||||
accountTxnLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,6 +662,9 @@
|
||||
agreementPricesMap.value = {}
|
||||
accountData.value = null
|
||||
productQuotas.value = []
|
||||
accountTxnRows.value = []
|
||||
accountTxnTotal.value = 0
|
||||
accountTxnPage.value = 1
|
||||
accountLoaded.value = false
|
||||
activeTab.value = 'basic'
|
||||
}
|
||||
@@ -672,69 +685,30 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
/* KPI 摘要条(对齐 ProtocolViewDialog:统一薄荷绿通栏 + 图标徽章) */
|
||||
/* KPI 摘要条 */
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
margin-bottom: 24px;
|
||||
padding: 6px;
|
||||
background: linear-gradient(135deg, #e8f4ee 0%, #f8fafc 52%, #e8f4ee 100%);
|
||||
border: 1px solid #a8d4c4;
|
||||
padding: 18px 20px;
|
||||
background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px 18px;
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid rgba(168, 212, 196, 0.75);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -746,8 +720,8 @@
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #a8d4c4;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: none;
|
||||
color: #065f46;
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -797,10 +771,6 @@
|
||||
&--white {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&--green {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@@ -892,44 +862,6 @@
|
||||
border-top: 1px dashed #e2e8f0;
|
||||
}
|
||||
|
||||
.sub-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.sub-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid rgba(0, 0, 0, 0.03);
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
}
|
||||
|
||||
.sub-card-header {
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1000,40 +932,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.agreement-price-list-wrap {
|
||||
margin-bottom: 0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.agreement-price-empty {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.agreement-price-table.list-table {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
.channel-view-txn-wrap {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__inner-wrapper) {
|
||||
border: none !important;
|
||||
|
||||
&::before {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__cell) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:last-child td.el-table__cell) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
.channel-view-txn-wrap .pagination.list-pager-bar {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
@@ -1065,11 +973,15 @@
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.account-quotas-head {
|
||||
.account-subsection-head {
|
||||
margin-bottom: 10px;
|
||||
|
||||
&--spaced {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.account-quotas-title {
|
||||
.account-subsection-title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -1088,38 +1000,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.account-quota-list-wrap {
|
||||
margin-bottom: 0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.account-quota-table.list-table {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__inner-wrapper) {
|
||||
border: none !important;
|
||||
|
||||
&::before {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table__cell) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:last-child td.el-table__cell) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.kpi-strip {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@@ -1138,10 +1018,6 @@
|
||||
.data-grid--4 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.sub-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
@@ -2504,42 +2504,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Sticky Tab 导航 ---- */
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin: 0 28px 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
SECTION CARDS
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
title="出库交接"
|
||||
width="1120px"
|
||||
width="1280px"
|
||||
class="dialog-form-layout outbound-handover-dialog"
|
||||
align-center
|
||||
append-to-body
|
||||
@@ -36,7 +36,7 @@
|
||||
<section class="section-card handover-form-section">
|
||||
<h3 class="section-title section-title--mint-bar">交接信息</h3>
|
||||
<el-form label-position="top" class="biz-dialog-form">
|
||||
<div class="form-grid-2">
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="交接人" required>
|
||||
<el-select
|
||||
v-model="handoverUserId"
|
||||
@@ -92,7 +92,7 @@
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="handoverRemark"
|
||||
type="textarea"
|
||||
|
||||
+9
-2
@@ -48,7 +48,14 @@
|
||||
</template>
|
||||
|
||||
<template #qc-info>
|
||||
<SampleQcInfoCards :qc-info="detail?.qcInfo" hide-sample-context />
|
||||
<SampleQcInfoGroupedPanel
|
||||
:gate-qc-info="detail?.qcInfo"
|
||||
:process-qc-info-list="detail?.processQcInfoList"
|
||||
gate-section-label="计划质检(出库门禁)"
|
||||
process-section-label="生产过程质检"
|
||||
:show-section-label="false"
|
||||
hide-sample-context
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #outbound-material>
|
||||
@@ -236,7 +243,7 @@
|
||||
type TagType
|
||||
} from '@/components/business/system-view-dialog/index.vue'
|
||||
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
|
||||
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
|
||||
import SampleQcInfoGroupedPanel from '@/components/business/sample-qc-info-grouped-panel/index.vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import ClinicalSchedulePaymentDetailView from '@/components/business/clinical-schedule-payment-detail/index.vue'
|
||||
import { fetchInfusionScheduleMgmtDetail } from '@/api/admin'
|
||||
|
||||
+195
-281
@@ -49,7 +49,13 @@
|
||||
max-height="280"
|
||||
empty-text="请从细胞产品库勾选"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="56" align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="productCode"
|
||||
label="产品编码"
|
||||
@@ -64,14 +70,20 @@
|
||||
/>
|
||||
<el-table-column prop="productTypeName" label="产品类型" min-width="100" />
|
||||
<el-table-column prop="specName" label="规格" min-width="90" />
|
||||
<el-table-column prop="passage" label="代次" min-width="70" align="center" />
|
||||
<el-table-column
|
||||
prop="passage"
|
||||
label="代次"
|
||||
min-width="70"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="storageSlotName"
|
||||
label="储物格"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="状态" min-width="90" align="center">
|
||||
<el-table-column label="状态" min-width="90" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.productStatus"
|
||||
@@ -83,9 +95,15 @@
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="80" align="center" fixed="right">
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link size="small" @click="removeCellRow($index)">
|
||||
<el-button type="danger" link @click="removeCellRow($index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
@@ -95,84 +113,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 使用的物料 -->
|
||||
<section class="section-card">
|
||||
<div class="section-head">
|
||||
<h3 class="section-title section-title--mint-bar">使用的物料</h3>
|
||||
<el-button type="primary" size="small" class="btn-main" @click="addMaterialRow">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加物料
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="materialRows.length"
|
||||
:data="materialRows"
|
||||
:row-key="(row) => row._rowKey"
|
||||
stripe
|
||||
class="list-table inner-table"
|
||||
max-height="280"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="56" align="center" />
|
||||
<el-table-column label="物料名称" min-width="180">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
v-model="row.materialId"
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
clearable
|
||||
placeholder="输入物料名称检索"
|
||||
:loading="materialOptionsLoading"
|
||||
:remote-method="remoteSearchMaterials"
|
||||
class="inner-cell-input"
|
||||
@visible-change="(visible) => visible && remoteSearchMaterials('')"
|
||||
@change="(v) => onMaterialSelect($index, v)"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in materialOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.label"
|
||||
:value="opt.id"
|
||||
:disabled="isMaterialOptionDisabled(opt.id, $index)"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格型号" min-width="120">
|
||||
<template #default="{ row }">{{ row.specModel || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="计划用量" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.qty"
|
||||
:min="0.0001"
|
||||
:precision="4"
|
||||
:step="1"
|
||||
controls-position="right"
|
||||
class="inner-cell-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" min-width="72" align="center">
|
||||
<template #default="{ row }">{{ row.unit || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="当前库存" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ stockQtyDisplay(row.materialId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link size="small" @click="removeMaterialRow($index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<p v-else class="section-empty-hint">点击「添加物料」从质检物料库勾选并填写计划用量</p>
|
||||
</section>
|
||||
|
||||
<!-- 纸质材料 -->
|
||||
<section class="section-card">
|
||||
<div class="section-head">
|
||||
@@ -182,48 +122,55 @@
|
||||
添加纸质材料
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="paperRows.length"
|
||||
:data="paperRows"
|
||||
:row-key="(row) => row._rowKey"
|
||||
stripe
|
||||
class="list-table inner-table"
|
||||
max-height="280"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="56" align="center" />
|
||||
<el-table-column label="名称" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.paperName"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="请选择或输入材料名称"
|
||||
class="inner-cell-input"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in PAPER_NAME_OPTIONS"
|
||||
:key="opt"
|
||||
:label="opt"
|
||||
:value="opt"
|
||||
<div class="list-body list-wrap inner-table-wrap">
|
||||
<el-table
|
||||
:data="paperRows"
|
||||
:row-key="(row) => row._rowKey"
|
||||
stripe
|
||||
class="list-table inner-table"
|
||||
max-height="280"
|
||||
empty-text="点击「添加纸质材料」逐条填写名称、份数并上传附件;仅作台账留存,不扣库存"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="名称" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.paperName"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
clearable
|
||||
placeholder="请选择或输入材料名称"
|
||||
class="inner-cell-input inner-cell-select"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in PAPER_NAME_OPTIONS"
|
||||
:key="opt"
|
||||
:label="opt"
|
||||
:value="opt"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="份数" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.copies"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
controls-position="right"
|
||||
class="inner-cell-input"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="份数" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.copies"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
controls-position="right"
|
||||
class="inner-cell-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" min-width="320">
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" min-width="320">
|
||||
<template #default="{ row, $index }">
|
||||
<div class="paper-att-cards">
|
||||
<div
|
||||
@@ -304,16 +251,22 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link size="small" @click="removePaperRow($index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<p v-else class="section-empty-hint">点击「添加纸质材料」逐条填写名称、份数并上传附件;仅作台账留存,不扣库存</p>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link @click="removePaperRow($index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-form label-position="top" class="biz-dialog-form">
|
||||
@@ -366,7 +319,7 @@
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="list-body list-wrap">
|
||||
<div class="list-body list-wrap inner-table-wrap">
|
||||
<el-table
|
||||
ref="cellSelectTableRef"
|
||||
class="list-table"
|
||||
@@ -381,14 +334,21 @@
|
||||
type="selection"
|
||||
min-width="45"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:selectable="isCellRowSelectable"
|
||||
/>
|
||||
<el-table-column prop="productCode" label="产品编码" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column prop="productName" label="产品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="productTypeName" label="产品类型" min-width="90" />
|
||||
<el-table-column prop="passage" label="代次" min-width="70" align="center" />
|
||||
<el-table-column
|
||||
prop="passage"
|
||||
label="代次"
|
||||
min-width="70"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column prop="storageSlotName" label="储物格" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="状态" min-width="90" align="center">
|
||||
<el-table-column label="状态" min-width="90" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="cellProductStatusTagType(row.productStatus) as any" size="small">
|
||||
{{ cellProductStatusLabel(row.productStatus) }}
|
||||
@@ -400,8 +360,17 @@
|
||||
</el-table>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="cellSelectVisible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!cellSelectedRows.length" @click="confirmCellSelect">
|
||||
<el-button class="btn-ghost" @click="cellSelectVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:disabled="!cellSelectedRows.length"
|
||||
@click="confirmCellSelect"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
确定(已选 {{ cellSelectedRows.length }} 项)
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -449,7 +418,6 @@
|
||||
} from '@element-plus/icons-vue'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { fetchCellProductList } from '@/api/cell-product'
|
||||
import { fetchQcMaterialList, type QcMaterialOption } from '@/api/qc-material'
|
||||
import {
|
||||
fetchOutboundReleaseMaterial,
|
||||
saveOutboundReleaseMaterial,
|
||||
@@ -480,13 +448,9 @@
|
||||
_uploading?: boolean
|
||||
}
|
||||
|
||||
type MaterialRow = {
|
||||
_rowKey: string
|
||||
type PreservedQcMaterial = {
|
||||
materialId: string
|
||||
materialName: string
|
||||
specModel: string
|
||||
unit: string
|
||||
qty?: number
|
||||
qty: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
@@ -518,10 +482,8 @@
|
||||
const submitting = ref(false)
|
||||
const formRemark = ref('')
|
||||
const cellRows = ref<OutboundReleaseCellRow[]>([])
|
||||
const materialRows = ref<MaterialRow[]>([])
|
||||
const preservedQcMaterials = ref<PreservedQcMaterial[]>([])
|
||||
const paperRows = ref<PaperRow[]>([])
|
||||
const materialOptions = ref<QcMaterialOption[]>([])
|
||||
const materialOptionsLoading = ref(false)
|
||||
|
||||
const cellSelectVisible = ref(false)
|
||||
const cellSearchKeyword = ref('')
|
||||
@@ -536,12 +498,7 @@
|
||||
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
let materialRowSeq = 0
|
||||
let paperRowSeq = 0
|
||||
function nextMaterialRowKey() {
|
||||
materialRowSeq += 1
|
||||
return `mr_${Date.now()}_${materialRowSeq}`
|
||||
}
|
||||
function nextPaperRowKey() {
|
||||
paperRowSeq += 1
|
||||
return `pr_${Date.now()}_${paperRowSeq}`
|
||||
@@ -575,101 +532,6 @@
|
||||
)
|
||||
}
|
||||
|
||||
function mapMaterialListToOptions(list: Array<Record<string, any>>): QcMaterialOption[] {
|
||||
return (list || []).map((m) => ({
|
||||
id: String(m.id || ''),
|
||||
label: `${m.materialName || ''}${m.materialCode ? ` (${m.materialCode})` : ''}`.trim(),
|
||||
materialCode: String(m.materialCode || ''),
|
||||
materialName: String(m.materialName || ''),
|
||||
specModel: String(m.specModel || ''),
|
||||
unit: String(m.unit || ''),
|
||||
stockQty: Number(m.stockQty ?? 0)
|
||||
}))
|
||||
}
|
||||
|
||||
function ensureMaterialsInOptions(rows: MaterialRow[]) {
|
||||
const merged = [...materialOptions.value]
|
||||
for (const row of rows) {
|
||||
const id = String(row.materialId || '')
|
||||
if (!id || merged.some((o) => o.id === id)) continue
|
||||
merged.push({
|
||||
id,
|
||||
label: row.materialName || id,
|
||||
materialCode: '',
|
||||
materialName: row.materialName || '',
|
||||
specModel: row.specModel || '',
|
||||
unit: row.unit || '',
|
||||
stockQty: 0
|
||||
})
|
||||
}
|
||||
materialOptions.value = merged
|
||||
}
|
||||
|
||||
async function remoteSearchMaterials(query: string) {
|
||||
materialOptionsLoading.value = true
|
||||
try {
|
||||
const res = await fetchQcMaterialList({
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: query?.trim() || undefined
|
||||
})
|
||||
materialOptions.value = mapMaterialListToOptions(res.list || [])
|
||||
ensureMaterialsInOptions(materialRows.value)
|
||||
} catch {
|
||||
materialOptions.value = []
|
||||
ensureMaterialsInOptions(materialRows.value)
|
||||
} finally {
|
||||
materialOptionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function stockQtyDisplay(materialId: string) {
|
||||
const opt = materialOptions.value.find((o) => o.id === materialId)
|
||||
if (!opt || opt.stockQty == null) return '—'
|
||||
return String(opt.stockQty)
|
||||
}
|
||||
|
||||
function isMaterialOptionDisabled(materialId: string, currentIndex: number) {
|
||||
return materialRows.value.some((r, i) => i !== currentIndex && r.materialId === materialId)
|
||||
}
|
||||
|
||||
function onMaterialSelect(index: number, id: string) {
|
||||
const row = materialRows.value[index]
|
||||
if (!row) return
|
||||
if (!id) {
|
||||
row.materialId = ''
|
||||
row.materialName = ''
|
||||
row.specModel = ''
|
||||
row.unit = ''
|
||||
return
|
||||
}
|
||||
if (isMaterialOptionDisabled(id, index)) {
|
||||
ElMessage.warning('该物料已在列表中,请勿重复添加')
|
||||
row.materialId = ''
|
||||
return
|
||||
}
|
||||
const opt = materialOptions.value.find((o) => o.id === id)
|
||||
row.materialId = id
|
||||
row.materialName = opt?.materialName || ''
|
||||
row.specModel = opt?.specModel || ''
|
||||
row.unit = opt?.unit || ''
|
||||
}
|
||||
|
||||
function addMaterialRow() {
|
||||
materialRows.value.push({
|
||||
_rowKey: nextMaterialRowKey(),
|
||||
materialId: '',
|
||||
materialName: '',
|
||||
specModel: '',
|
||||
unit: '',
|
||||
qty: undefined
|
||||
})
|
||||
}
|
||||
|
||||
function removeMaterialRow(index: number) {
|
||||
materialRows.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function addPaperRow() {
|
||||
paperRows.value.push({
|
||||
_rowKey: nextPaperRowKey(),
|
||||
@@ -800,17 +662,14 @@
|
||||
const data = await fetchOutboundReleaseMaterial(props.scheduleId)
|
||||
formRemark.value = data.remark || ''
|
||||
cellRows.value = (data.cells || []).map((c) => ({ ...c }))
|
||||
materialRows.value = (data.qcMaterials || []).map((m) => ({
|
||||
_rowKey: nextMaterialRowKey(),
|
||||
materialId: m.qcMaterialId || '',
|
||||
materialName: m.materialName || '',
|
||||
specModel: m.specModel || '',
|
||||
unit: m.unit || '',
|
||||
qty: m.qty,
|
||||
remark: m.remark || ''
|
||||
}))
|
||||
preservedQcMaterials.value = (data.qcMaterials || [])
|
||||
.map((m) => ({
|
||||
materialId: m.qcMaterialId || '',
|
||||
qty: Number(m.qty),
|
||||
remark: m.remark || undefined
|
||||
}))
|
||||
.filter((m) => m.materialId && Number.isFinite(m.qty) && m.qty > 0)
|
||||
paperRows.value = mapPaperRowsFromApi(data.paperMaterials, data.paperAttachments)
|
||||
await remoteSearchMaterials('')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -826,7 +685,7 @@
|
||||
|
||||
function resetState() {
|
||||
cellRows.value = []
|
||||
materialRows.value = []
|
||||
preservedQcMaterials.value = []
|
||||
paperRows.value = []
|
||||
formRemark.value = ''
|
||||
cellSelectVisible.value = false
|
||||
@@ -883,18 +742,6 @@
|
||||
return
|
||||
}
|
||||
|
||||
for (const row of materialRows.value) {
|
||||
if (!row.materialId) {
|
||||
ElMessage.warning('请为每条物料选择名称')
|
||||
return
|
||||
}
|
||||
const qty = row.qty
|
||||
if (qty == null || !Number.isFinite(Number(qty)) || Number(qty) <= 0) {
|
||||
ElMessage.warning('物料计划用量须大于 0')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of paperRows.value) {
|
||||
if (!row.paperName?.trim()) {
|
||||
ElMessage.warning('请填写每条纸质材料的名称')
|
||||
@@ -924,11 +771,7 @@
|
||||
cellProductId: String(c.cellProductId || ''),
|
||||
remark: c.remark
|
||||
})),
|
||||
qcMaterials: materialRows.value.map((m) => ({
|
||||
materialId: m.materialId,
|
||||
qty: Number(m.qty),
|
||||
remark: m.remark
|
||||
}))
|
||||
qcMaterials: preservedQcMaterials.value
|
||||
})
|
||||
ElMessage.success('出库物料准备已保存')
|
||||
emit('update:modelValue', false)
|
||||
@@ -995,10 +838,81 @@
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.section-empty-hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
.section-title--mint-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
background: var(--primary, #047857);
|
||||
border-radius: 2px;
|
||||
margin-right: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inner-table-wrap.list-body.list-wrap {
|
||||
border-top: 1px solid var(--mint-border, #e2e8f0);
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.el-table) {
|
||||
--el-table-header-bg-color: var(--mint-table-head, #f8fafc);
|
||||
--el-table-border-color: var(--mint-border, #e2e8f0);
|
||||
--el-table-row-hover-bg-color: var(--mint-row-hover, #f0fdf4);
|
||||
--el-table-text-color: var(--mint-text, #0f172a);
|
||||
--el-table-header-text-color: var(--mint-text-muted, #64748b);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell) {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
:deep(.el-table td.el-table__cell) {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text, #0f172a);
|
||||
}
|
||||
|
||||
:deep(.el-empty) {
|
||||
padding: 24px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inner-table {
|
||||
width: 100%;
|
||||
|
||||
:deep(td.el-table__cell) {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.inner-cell-input) {
|
||||
width: 100%;
|
||||
|
||||
.el-input__wrapper {
|
||||
min-height: 36px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.el-input-number .el-input__wrapper {
|
||||
min-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.inner-cell-select) {
|
||||
width: 100%;
|
||||
|
||||
.el-select__wrapper {
|
||||
min-height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inner-cell-input {
|
||||
|
||||
+417
@@ -0,0 +1,417 @@
|
||||
<!-- 出库放行审核 · 卡片列表 -->
|
||||
<template>
|
||||
<div class="list-body list-wrap outbound-release-review-cards-body">
|
||||
<div v-loading="loading" class="outbound-release-review-cards-wrap">
|
||||
<article
|
||||
v-for="(row, idx) in rows"
|
||||
:key="String(row.scheduleId || row.id || idx)"
|
||||
class="outbound-release-review-card"
|
||||
@dblclick="emit('card-dblclick', row)"
|
||||
>
|
||||
<header class="outbound-release-review-card__head">
|
||||
<div class="outbound-release-review-card__head-main">
|
||||
<div class="outbound-release-review-card__title-row">
|
||||
<el-checkbox
|
||||
:model-value="isSelected(row)"
|
||||
class="outbound-release-review-card__checkbox"
|
||||
@change="(val) => emit('toggle-select', row, Boolean(val))"
|
||||
@click.stop
|
||||
/>
|
||||
<span class="outbound-release-review-card__index">{{ cardIndex(idx) }}</span>
|
||||
<span class="outbound-release-review-card__code">{{ row.applicationNo || '—' }}</span>
|
||||
<el-tag v-if="row.bizType" :type="bizTypeTagType(row.bizType) as any" size="small">
|
||||
{{ row.bizTypeLabel || bizTypeLabel(row.bizType) }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="row.scheduleStatus"
|
||||
:type="scheduleStatusTagType(row.scheduleStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ scheduleStatusLabel(row.scheduleStatus) }}
|
||||
</el-tag>
|
||||
<el-tag v-if="row.qcStatus" :type="qcStatusTagType(row.qcStatus)" size="small">
|
||||
{{ qcStatusLabel(row.qcStatus) }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="row.reviewConclusion"
|
||||
:type="storageReleaseConclusionTagType(row.reviewConclusion)"
|
||||
size="small"
|
||||
>
|
||||
{{ storageReleaseConclusionLabel(row.reviewConclusion) }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="small">待审核</el-tag>
|
||||
</div>
|
||||
<dl class="outbound-release-review-card__meta">
|
||||
<div class="outbound-release-review-card__meta-item">
|
||||
<dt>患者姓名</dt>
|
||||
<dd>{{ row.patientName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="outbound-release-review-card__meta-item">
|
||||
<dt>{{ pickupTimeLabel }}</dt>
|
||||
<dd>{{ row.infusionTime || '—' }}</dd>
|
||||
</div>
|
||||
<div class="outbound-release-review-card__meta-item">
|
||||
<dt>审核人</dt>
|
||||
<dd>{{ row.reviewByName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="outbound-release-review-card__meta-item">
|
||||
<dt>审核时间</dt>
|
||||
<dd>{{ row.reviewTime || '—' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<section class="outbound-release-review-card__cells">
|
||||
<div class="outbound-release-review-card__cells-title">需求细胞</div>
|
||||
<div v-if="cellProducts(row).length" class="outbound-release-review-card__cell-list">
|
||||
<div
|
||||
v-for="(product, pi) in cellProducts(row)"
|
||||
:key="`${product.projectCode || product.projectName || pi}`"
|
||||
class="outbound-release-review-card__cell-item"
|
||||
>
|
||||
<dl class="outbound-release-review-card__cell-fields">
|
||||
<div class="outbound-release-review-card__cell-field">
|
||||
<dt>项目编码</dt>
|
||||
<dd>{{ product.projectCode || '—' }}</dd>
|
||||
</div>
|
||||
<div class="outbound-release-review-card__cell-field">
|
||||
<dt>项目名称</dt>
|
||||
<dd>{{ product.projectName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="outbound-release-review-card__cell-field">
|
||||
<dt>细胞类型</dt>
|
||||
<dd>{{ product.cellTypeName || row.cellType || '—' }}</dd>
|
||||
</div>
|
||||
<div class="outbound-release-review-card__cell-field">
|
||||
<dt>规格</dt>
|
||||
<dd>{{ product.specText || row.specText || '—' }}</dd>
|
||||
</div>
|
||||
<div class="outbound-release-review-card__cell-field">
|
||||
<dt>数量</dt>
|
||||
<dd>{{ product.quantity != null ? product.quantity : '—' }}</dd>
|
||||
</div>
|
||||
<div class="outbound-release-review-card__cell-field">
|
||||
<dt>产品状态</dt>
|
||||
<dd>
|
||||
<el-tag
|
||||
v-if="product.productStatus"
|
||||
:type="productStatusTagType(product.productStatus) as any"
|
||||
size="small"
|
||||
>
|
||||
{{ productStatusLabel(product.productStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>—</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="outbound-release-review-card__cells-fallback">
|
||||
<template v-if="row.cellType || row.specText">
|
||||
<span v-if="row.cellType">细胞类型:{{ row.cellType }}</span>
|
||||
<span v-if="row.cellType && row.specText"> · </span>
|
||||
<span v-if="row.specText">规格:{{ row.specText }}</span>
|
||||
</template>
|
||||
<template v-else>暂无细胞需求明细</template>
|
||||
</p>
|
||||
</section>
|
||||
<div class="outbound-release-review-card__ops">
|
||||
<el-button class="btn-op-item" @click.stop="emit('view', row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click.stop="emit('material', row)">
|
||||
<el-icon><Box /></el-icon>出库物料
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click.stop="emit('review', row)">
|
||||
<el-icon><CircleCheck /></el-icon>审核
|
||||
</el-button>
|
||||
</div>
|
||||
</header>
|
||||
</article>
|
||||
<el-empty
|
||||
v-if="!loading && !rows.length"
|
||||
class="outbound-release-review-cards-empty"
|
||||
description="暂无出库放行审核记录"
|
||||
:image-size="72"
|
||||
/>
|
||||
</div>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(v) => emit('page-size-change', v)"
|
||||
@current-change="(v) => emit('page-change', v)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { View, CircleCheck, Box } from '@element-plus/icons-vue'
|
||||
import type { OutboundReleaseReview } from '@/api/outbound-release-review'
|
||||
import type { OutboundQcDetailProduct } from '@/api/qc-work-order'
|
||||
import {
|
||||
storageReleaseConclusionLabel,
|
||||
storageReleaseConclusionTagType,
|
||||
scheduleStatusLabel,
|
||||
scheduleStatusTagType,
|
||||
qcStatusLabel,
|
||||
qcStatusTagType,
|
||||
bizTypeLabel,
|
||||
bizTypeTagType,
|
||||
productStatusLabel,
|
||||
productStatusTagType
|
||||
} from '@/utils/status-tag-registry'
|
||||
|
||||
defineOptions({ name: 'OutboundReleaseReviewCardList' })
|
||||
|
||||
const props = defineProps<{
|
||||
rows: OutboundReleaseReview[]
|
||||
loading: boolean
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
selectedIds: string[]
|
||||
pickupTimeLabel: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
view: [row: OutboundReleaseReview]
|
||||
material: [row: OutboundReleaseReview]
|
||||
review: [row: OutboundReleaseReview]
|
||||
'toggle-select': [row: OutboundReleaseReview, checked: boolean]
|
||||
'card-dblclick': [row: OutboundReleaseReview]
|
||||
'page-change': [page: number]
|
||||
'page-size-change': [size: number]
|
||||
}>()
|
||||
|
||||
function rowId(row: OutboundReleaseReview) {
|
||||
return String(row.scheduleId || row.id || '')
|
||||
}
|
||||
|
||||
function isSelected(row: OutboundReleaseReview) {
|
||||
return props.selectedIds.includes(rowId(row))
|
||||
}
|
||||
|
||||
function cardIndex(i: number) {
|
||||
return (props.page - 1) * props.pageSize + i + 1
|
||||
}
|
||||
|
||||
function cellProducts(row: OutboundReleaseReview): OutboundQcDetailProduct[] {
|
||||
return row.products?.length ? row.products : []
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.outbound-release-review-cards-body {
|
||||
border-top: 1px solid var(--mint-border, #e2e8f0);
|
||||
}
|
||||
|
||||
.outbound-release-review-cards-wrap {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(420px, 1fr));
|
||||
gap: 14px;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.outbound-release-review-card {
|
||||
min-width: 0;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 1px 3px rgba(15, 23, 42, 0.06),
|
||||
0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #a8d4c4;
|
||||
box-shadow: 0 4px 12px rgba(4, 120, 87, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.outbound-release-review-card__head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.outbound-release-review-card__head-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.outbound-release-review-card__title-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.outbound-release-review-card__checkbox {
|
||||
flex-shrink: 0;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.outbound-release-review-card__index {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.outbound-release-review-card__code {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.outbound-release-review-card__meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.outbound-release-review-card__meta-item {
|
||||
min-width: 0;
|
||||
|
||||
dt {
|
||||
margin: 0 0 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.outbound-release-review-card__cells {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #d1fae5;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.outbound-release-review-card__cells-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #047857;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 14px;
|
||||
margin-right: 8px;
|
||||
background: #047857;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.outbound-release-review-card__cell-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.outbound-release-review-card__cell-item {
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
|
||||
&:not(:last-child) {
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.outbound-release-review-card__cell-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.outbound-release-review-card__cell-field {
|
||||
min-width: 0;
|
||||
|
||||
dt {
|
||||
margin: 0 0 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.outbound-release-review-card__cells-fallback {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.outbound-release-review-card__ops {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.outbound-release-review-cards-empty {
|
||||
grid-column: 1 / -1;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.outbound-release-review-cards-wrap {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.outbound-release-review-card__meta,
|
||||
.outbound-release-review-card__cell-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+203
-31
@@ -4,8 +4,8 @@
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="640px"
|
||||
class="dialog-form-layout"
|
||||
width="720px"
|
||||
class="dialog-form-layout outbound-release-review-dialog"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
@@ -15,19 +15,49 @@
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<div v-if="isSingleMode && singleDetail" class="review-info-summary">
|
||||
<div class="review-summary-row">
|
||||
<span class="review-summary-label">申请单号</span>
|
||||
<span class="review-summary-value">{{ singleDetail.application?.applicationNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="review-summary-row">
|
||||
<span class="review-summary-label">患者姓名</span>
|
||||
<span class="review-summary-value">{{ singleDetail.patient?.patientName || '—' }}</span>
|
||||
</div>
|
||||
<div class="review-summary-row">
|
||||
<span class="review-summary-label">细胞类型</span>
|
||||
<span class="review-summary-value">{{ singleDetail.infusionPlan?.cellType || '—' }}</span>
|
||||
<div v-if="isSingleMode && singleDetail" class="review-context-card">
|
||||
<div class="review-context-card__hero">
|
||||
<div class="review-context-card__hero-main">
|
||||
<span class="review-context-card__eyebrow">申请单号</span>
|
||||
<span class="review-context-card__code">{{
|
||||
singleDetail.application?.applicationNo || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="review-context-card__tags">
|
||||
<el-tag
|
||||
v-if="reviewBizTypeLabel"
|
||||
type="primary"
|
||||
size="small"
|
||||
>
|
||||
{{ reviewBizTypeLabel }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="singleDetail.infusionSchedule?.scheduleStatus"
|
||||
:type="reviewScheduleStatusTag as any"
|
||||
size="small"
|
||||
>
|
||||
{{ reviewScheduleStatusLabel }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="singleDetail.infusionSchedule?.qcStatus"
|
||||
:type="reviewQcStatusTag as any"
|
||||
size="small"
|
||||
>
|
||||
{{ reviewQcStatusLabel }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="review-context-grid">
|
||||
<div
|
||||
v-for="item in reviewContextItems"
|
||||
:key="item.label"
|
||||
class="review-context-kv"
|
||||
:class="{ 'review-context-kv--span': item.span }"
|
||||
>
|
||||
<dt>{{ item.label }}</dt>
|
||||
<dd>{{ item.value }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<p v-else-if="scheduleIds.length > 1" class="audit-target-hint">
|
||||
将对已选
|
||||
@@ -188,6 +218,14 @@
|
||||
batchReviewOutboundRelease,
|
||||
type OutboundReleaseReviewDetail
|
||||
} from '@/api/outbound-release-review'
|
||||
import { getPilotBizConfig, resolvePilotBizType } from '@/config/pilot-biz-type'
|
||||
import {
|
||||
bizTypeLabel,
|
||||
qcStatusLabel,
|
||||
qcStatusTagType,
|
||||
scheduleStatusLabel,
|
||||
scheduleStatusTagType
|
||||
} from '@/utils/status-tag-registry'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
|
||||
type AttachmentItem = {
|
||||
@@ -211,12 +249,83 @@
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const attachmentUploadRef = ref()
|
||||
const submitting = ref(false)
|
||||
const singleDetail = ref<OutboundReleaseReviewDetail | null>(null)
|
||||
const attachmentFiles = ref<AttachmentItem[]>([])
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewType = ref<'image' | 'video'>('image')
|
||||
|
||||
const isSingleMode = computed(() => props.scheduleIds.length === 1)
|
||||
|
||||
const reviewBizType = computed(() =>
|
||||
resolvePilotBizType(singleDetail.value?.application?.bizType)
|
||||
)
|
||||
const reviewBizConfig = computed(() => getPilotBizConfig(reviewBizType.value))
|
||||
const reviewBizTypeLabel = computed(() => {
|
||||
const fromApi = singleDetail.value?.application?.bizTypeLabel
|
||||
if (fromApi) return fromApi
|
||||
const code = singleDetail.value?.application?.bizType
|
||||
return code ? bizTypeLabel(code) : ''
|
||||
})
|
||||
const reviewScheduleStatusLabel = computed(() =>
|
||||
scheduleStatusLabel(singleDetail.value?.infusionSchedule?.scheduleStatus || '')
|
||||
)
|
||||
const reviewScheduleStatusTag = computed(() =>
|
||||
scheduleStatusTagType(singleDetail.value?.infusionSchedule?.scheduleStatus || '')
|
||||
)
|
||||
const reviewQcStatusLabel = computed(() =>
|
||||
qcStatusLabel(singleDetail.value?.infusionSchedule?.qcStatus || '')
|
||||
)
|
||||
const reviewQcStatusTag = computed(() =>
|
||||
qcStatusTagType(singleDetail.value?.infusionSchedule?.qcStatus || '')
|
||||
)
|
||||
|
||||
function uniqueJoined(values: Array<string | undefined | null>) {
|
||||
const items = [...new Set(values.map((v) => String(v || '').trim()).filter(Boolean))]
|
||||
return items.length ? items.join('、') : '—'
|
||||
}
|
||||
|
||||
function resolveReviewCellType(detail: OutboundReleaseReviewDetail | null) {
|
||||
if (!detail) return '—'
|
||||
const planType = detail.infusionPlan?.cellType?.trim()
|
||||
if (planType) return planType
|
||||
return uniqueJoined((detail.products || []).map((p) => p.cellTypeName))
|
||||
}
|
||||
|
||||
function resolveReviewSpecText(detail: OutboundReleaseReviewDetail | null) {
|
||||
if (!detail) return '—'
|
||||
const planSpec = detail.infusionPlan?.specText?.trim()
|
||||
if (planSpec) return planSpec
|
||||
return uniqueJoined((detail.products || []).map((p) => p.specText))
|
||||
}
|
||||
|
||||
const reviewContextItems = computed(() => {
|
||||
const d = singleDetail.value
|
||||
if (!d) return [] as Array<{ label: string; value: string; span?: boolean }>
|
||||
const pickupLabel = reviewBizConfig.value.pickupTimeLabel
|
||||
const isBasicExperiment = reviewBizType.value === 'basic_experiment'
|
||||
const items: Array<{ label: string; value: string; span?: boolean }> = [
|
||||
{ label: '患者姓名', value: d.patient?.patientName || '—' },
|
||||
{ label: '细胞类型', value: resolveReviewCellType(d) },
|
||||
{ label: '规格', value: resolveReviewSpecText(d) },
|
||||
{ label: pickupLabel, value: d.infusionSchedule?.infusionTime || '—' }
|
||||
]
|
||||
if (!isBasicExperiment) {
|
||||
items.push(
|
||||
{ label: '输注地点', value: d.infusionSchedule?.infusionLocation || '—' },
|
||||
{ label: '渠道名称', value: d.application?.channelName || '—' }
|
||||
)
|
||||
} else {
|
||||
items.push(
|
||||
{ label: '课题名称', value: d.application?.projectTitle || '—', span: true },
|
||||
{ label: '客户名称', value: d.application?.customerName || '—' }
|
||||
)
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
reviewConclusion: 'tg',
|
||||
reviewRemark: ''
|
||||
@@ -395,31 +504,94 @@
|
||||
}
|
||||
}
|
||||
|
||||
.review-info-summary {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px 20px;
|
||||
.review-context-card {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #d1fae5;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #ecfdf5 0%, #fff 42%);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
}
|
||||
|
||||
.review-summary-row {
|
||||
.review-context-card__hero {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px 12px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.review-summary-label {
|
||||
color: var(--mint-text-muted, #334155);
|
||||
.review-context-card__hero-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.review-context-card__eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.review-context-card__code {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: #0f172a;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.review-context-card__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.review-summary-value {
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
.review-context-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 20px;
|
||||
margin: 0;
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
.review-context-kv {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
|
||||
&--span {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
dt {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.review-context-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.form-col-span-2 {
|
||||
|
||||
+39
-2
@@ -14,12 +14,27 @@
|
||||
append-to-body
|
||||
modal-class="outbound-release-view-dialog-mask"
|
||||
>
|
||||
<template #diagnosis-attachments>
|
||||
<OutboundQcDiagnosisAttachments :files="diagnosisAttachmentFiles" />
|
||||
</template>
|
||||
|
||||
<template #product-table>
|
||||
<OutboundQcProductTable :products="viewProducts" />
|
||||
</template>
|
||||
|
||||
<template #production-info>
|
||||
<SampleProductionInfoCards :production-info="detail?.productionInfo" />
|
||||
</template>
|
||||
|
||||
<template #qc-info>
|
||||
<SampleQcInfoCards :qc-info="detail?.qcInfo" hide-sample-context />
|
||||
<SampleQcInfoGroupedPanel
|
||||
:gate-qc-info="detail?.qcInfo"
|
||||
:process-qc-info-list="detail?.processQcInfoList"
|
||||
gate-section-label="计划质检(出库门禁)"
|
||||
process-section-label="生产过程质检"
|
||||
:show-section-label="false"
|
||||
hide-sample-context
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #review-attachments>
|
||||
@@ -223,8 +238,10 @@
|
||||
import { CircleCheck, Document, Download, View } from '@element-plus/icons-vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
|
||||
import SampleQcInfoGroupedPanel from '@/components/business/sample-qc-info-grouped-panel/index.vue'
|
||||
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
|
||||
import OutboundQcDiagnosisAttachments from '@/views/quality-inspection/qc-work-order/view-dialogs/OutboundQcDiagnosisAttachments.vue'
|
||||
import OutboundQcProductTable from '@/views/quality-inspection/qc-work-order/view-dialogs/OutboundQcProductTable.vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import {
|
||||
fetchOutboundReleaseReviewDetail,
|
||||
@@ -298,6 +315,26 @@
|
||||
return resolvePilotDetail(detail.value).application?.applicationNo || ''
|
||||
})
|
||||
|
||||
const viewProducts = computed(() => {
|
||||
if (!detail.value) return []
|
||||
return resolvePilotDetail(detail.value).products || []
|
||||
})
|
||||
|
||||
const diagnosisAttachmentFiles = computed(() => {
|
||||
if (!detail.value) return []
|
||||
const urls = resolvePilotDetail(detail.value).patient?.diagnosisAttachments || ''
|
||||
if (!urls.trim()) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.map((u) => u.trim())
|
||||
.filter(Boolean)
|
||||
.map((fileUrl, i) => ({
|
||||
id: `diag_${i}`,
|
||||
fileUrl,
|
||||
fileName: fileUrl.split('/').pop() || ''
|
||||
}))
|
||||
})
|
||||
|
||||
const reviewAttachmentFiles = computed(() => {
|
||||
const urls = detail.value?.reviewInfo?.reviewAttachments || ''
|
||||
if (!urls.trim()) return []
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>支持按申请单号、患者姓名搜索;{{ pickupTimeLabel }}默认今天前 15 天至后 15 天。</span
|
||||
>支持按申请单号、患者姓名搜索。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
@@ -74,148 +74,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table outbound-release-review-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="50" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
prop="applicationNo"
|
||||
label="申请单号"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="patientName"
|
||||
label="患者姓名"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="bizType"
|
||||
label="类型"
|
||||
min-width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.bizType" :type="bizTypeTagType(row.bizType) as any" size="small">
|
||||
{{ row.bizTypeLabel || bizTypeLabel(row.bizType) }}
|
||||
</el-tag>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="infusionTime"
|
||||
:label="pickupTimeLabel"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="scheduleStatus"
|
||||
label="计划状态"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.scheduleStatus"
|
||||
:type="scheduleStatusTagType(row.scheduleStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ scheduleStatusLabel(row.scheduleStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="qcStatus"
|
||||
label="质检状态"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.qcStatus" :type="qcStatusTagType(row.qcStatus)" size="small">
|
||||
{{ qcStatusLabel(row.qcStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="reviewConclusion"
|
||||
label="审核结论"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.reviewConclusion"
|
||||
:type="storageReleaseConclusionTagType(row.reviewConclusion)"
|
||||
size="small"
|
||||
>
|
||||
{{ storageReleaseConclusionLabel(row.reviewConclusion) }}
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="small">待审核</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="reviewByName"
|
||||
label="审核人"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="reviewTime"
|
||||
label="审核时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="260"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="openMaterialDialog(row)">
|
||||
<el-icon><Box /></el-icon>出库物料
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="openReviewDialog(row)">
|
||||
<el-icon><CircleCheck /></el-icon>审核
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<OutboundReleaseReviewCardList
|
||||
:rows="tableData"
|
||||
:loading="loading"
|
||||
:page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:selected-ids="selectedIds"
|
||||
:pickup-time-label="pickupTimeLabel"
|
||||
@toggle-select="handleToggleSelect"
|
||||
@card-dblclick="handleView"
|
||||
@view="handleView"
|
||||
@material="openMaterialDialog"
|
||||
@review="openReviewDialog"
|
||||
@page-change="(val) => ((pagination.page = val), loadData())"
|
||||
@page-size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
/>
|
||||
|
||||
<OutboundReleaseViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
@@ -242,24 +116,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { View, Search, RefreshLeft, CircleCheck, Box } from '@element-plus/icons-vue'
|
||||
import { Search, RefreshLeft, CircleCheck } from '@element-plus/icons-vue'
|
||||
import OutboundReleaseViewDialog from './OutboundReleaseViewDialog.vue'
|
||||
import OutboundReleaseReviewDialog from './OutboundReleaseReviewDialog.vue'
|
||||
import OutboundReleaseMaterialDialog from './OutboundReleaseMaterialDialog.vue'
|
||||
import OutboundReleaseReviewCardList from './OutboundReleaseReviewCardList.vue'
|
||||
import {
|
||||
fetchOutboundReleaseReviewList,
|
||||
type OutboundReleaseReview
|
||||
} from '@/api/outbound-release-review'
|
||||
import {
|
||||
storageReleaseConclusionLabel,
|
||||
storageReleaseConclusionTagType,
|
||||
scheduleStatusLabel,
|
||||
scheduleStatusTagType,
|
||||
qcStatusLabel,
|
||||
qcStatusTagType,
|
||||
bizTypeLabel,
|
||||
bizTypeTagType
|
||||
} from '@/utils/status-tag-registry'
|
||||
import { getPilotBizConfig, resolvePilotBizType } from '@/config/pilot-biz-type'
|
||||
|
||||
defineOptions({ name: 'PilotServiceOutboundReleaseReview' })
|
||||
@@ -278,35 +143,33 @@
|
||||
{ value: 'btg', label: '不通过' }
|
||||
]
|
||||
|
||||
function formatLocalDate(d: 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}`
|
||||
}
|
||||
|
||||
/** 输注时间默认:今天前 15 天 ~ 今天后 15 天 */
|
||||
function getDefaultInfusionTimeRange(): [string, string] {
|
||||
const today = new Date()
|
||||
const start = new Date(today)
|
||||
start.setDate(start.getDate() - 15)
|
||||
const end = new Date(today)
|
||||
end.setDate(end.getDate() + 15)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const searchForm = reactive({
|
||||
reviewConclusion: '',
|
||||
keyword: '',
|
||||
infusionTimeRange: getDefaultInfusionTimeRange() as [string, string]
|
||||
infusionTimeRange: null as [string, string] | null
|
||||
})
|
||||
const tableData = ref<OutboundReleaseReview[]>([])
|
||||
const selectedRows = ref<OutboundReleaseReview[]>([])
|
||||
const selectedIds = computed(() =>
|
||||
selectedRows.value.map((r) => String(r.scheduleId || r.id || ''))
|
||||
)
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
const handleSelectionChange = (rows: OutboundReleaseReview[]) => {
|
||||
selectedRows.value = rows
|
||||
function rowId(row: OutboundReleaseReview) {
|
||||
return String(row.scheduleId || row.id || '')
|
||||
}
|
||||
|
||||
function handleToggleSelect(row: OutboundReleaseReview, checked: boolean) {
|
||||
const id = rowId(row)
|
||||
if (!id) return
|
||||
if (checked) {
|
||||
if (!selectedRows.value.some((r) => rowId(r) === id)) {
|
||||
selectedRows.value = [...selectedRows.value, row]
|
||||
}
|
||||
return
|
||||
}
|
||||
selectedRows.value = selectedRows.value.filter((r) => rowId(r) !== id)
|
||||
}
|
||||
|
||||
const canBatchReview = computed(() => selectedRows.value.length > 0)
|
||||
@@ -338,7 +201,7 @@
|
||||
function handleReset() {
|
||||
searchForm.reviewConclusion = ''
|
||||
searchForm.keyword = ''
|
||||
searchForm.infusionTimeRange = getDefaultInfusionTimeRange()
|
||||
searchForm.infusionTimeRange = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
@@ -401,13 +264,3 @@
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.outbound-release-review-table {
|
||||
:deep(td.el-table__cell:has(.btn-op-item)),
|
||||
:deep(td.el-table-fixed-column--right:has(.btn-op-item)),
|
||||
:deep(th.el-table-fixed-column--right) {
|
||||
min-width: 260px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3455,42 +3455,6 @@
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* ---- Sticky Tab 导航 ---- */
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin: 0 28px 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- KPI 摘要条 ---- */
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
|
||||
@@ -2875,42 +2875,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Sticky Tab 导航 ---- */
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin: 0 28px 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
|
||||
@@ -1510,57 +1510,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active):not(.is-disabled) {
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: default;
|
||||
opacity: 0.65;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
margin-bottom: 24px;
|
||||
padding: 6px;
|
||||
background: linear-gradient(135deg, #e8f4ee 0%, #f8fafc 52%, #e8f4ee 100%);
|
||||
border: 1px solid #a8d4c4;
|
||||
padding: 18px 20px;
|
||||
background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
||||
box-shadow: none;
|
||||
|
||||
&--cols5 {
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
@@ -1571,12 +1530,13 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px 18px;
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid rgba(168, 212, 196, 0.75);
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1588,8 +1548,8 @@
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #a8d4c4;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: none;
|
||||
color: #065f46;
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -1618,6 +1578,7 @@
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #047857;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1660,16 +1621,12 @@
|
||||
&--banner {
|
||||
margin: -22px -24px 18px;
|
||||
padding: 10px 24px;
|
||||
border-bottom: none;
|
||||
border-bottom: 1px solid rgba(16, 185, 129, 0.12);
|
||||
border-radius: 12px 12px 0 0;
|
||||
background: var(--primary, #10b981);
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
|
||||
h3 {
|
||||
color: #fff;
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -415,20 +415,11 @@
|
||||
: '支持申请单号、患者姓名、渠道名称模糊查询,输入即检索。'
|
||||
)
|
||||
|
||||
// ── 搜索(输注时间默认最近 30 天) ──
|
||||
const getDefaultTimeRange = () => {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setTime(start.getTime() - 30 * 24 * 3600 * 1000)
|
||||
const fmt = (d: Date) => d.toISOString().slice(0, 10)
|
||||
return [fmt(start), fmt(end)]
|
||||
}
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
infusionStatus: '',
|
||||
qcStatus: '',
|
||||
infusionTimeRange: getDefaultTimeRange() as string[]
|
||||
infusionTimeRange: null as string[] | null
|
||||
})
|
||||
|
||||
// ── 表格 ─────────────────────────────
|
||||
@@ -530,7 +521,7 @@
|
||||
keyword: '',
|
||||
infusionStatus: '',
|
||||
qcStatus: '',
|
||||
infusionTimeRange: getDefaultTimeRange()
|
||||
infusionTimeRange: null
|
||||
})
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
|
||||
@@ -109,141 +109,3 @@
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pill-tab-nav {
|
||||
padding: 16px 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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: var(--mint-text-muted, #334155);
|
||||
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: var(--mint-text-muted, #334155);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@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>
|
||||
@@ -1800,41 +1800,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin: 0 28px 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tab-pane {
|
||||
padding: 0 28px 4px;
|
||||
|
||||
|
||||
@@ -710,41 +710,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin: 0 28px 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tab-pane {
|
||||
padding: 0 28px 4px;
|
||||
|
||||
|
||||
+3
-23
@@ -21,20 +21,6 @@ import { getCardMetaConfig } from '@/utils/production-work-order-card-meta'
|
||||
|
||||
export type ProductionWorkOrderTabPageMode = 'manage' | 'mine'
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultLast30DaysRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
export function useProductionWorkOrderTabPage(
|
||||
tabKey: Ref<ProductionWorkOrderTabKey>,
|
||||
pageMode: Ref<ProductionWorkOrderTabPageMode> | ProductionWorkOrderTabPageMode
|
||||
@@ -46,13 +32,7 @@ export function useProductionWorkOrderTabPage(
|
||||
const filterTimeLabel = computed(() => filterTimeLabelForTab(tabConfig.value))
|
||||
const filterTimeHint = computed(() => {
|
||||
if (pageModeValue.value === 'mine') {
|
||||
const tab = tabConfig.value
|
||||
const timeHint = tabUsesInfusionTimeFilter(tab)
|
||||
? tab.key === 'outbound_basic_experiment'
|
||||
? '默认展示近 30 天内交付的工单。'
|
||||
: '默认展示近 30 天内输注的工单。'
|
||||
: '默认展示近 30 天内创建的工单。'
|
||||
return `仅展示指派给当前登录人的生产任务(含已转出记录,转出任务仅可查看)。${timeHint}`
|
||||
return '仅展示指派给当前登录人的生产任务(含已转出记录,转出任务仅可查看)。'
|
||||
}
|
||||
return filterTimeHintForTab(tabConfig.value)
|
||||
})
|
||||
@@ -62,7 +42,7 @@ export function useProductionWorkOrderTabPage(
|
||||
workOrderStatus: '',
|
||||
taskStatus: ''
|
||||
})
|
||||
const createTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
|
||||
const createTimeRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<ProductionWorkOrderItem[]>([])
|
||||
const presetTasksByWorkOrderId = ref<Record<string, Record<string, any>[]>>({})
|
||||
const loading = ref(false)
|
||||
@@ -150,7 +130,7 @@ export function useProductionWorkOrderTabPage(
|
||||
searchForm.keyword = ''
|
||||
searchForm.workOrderStatus = ''
|
||||
searchForm.taskStatus = ''
|
||||
createTimeRange.value = getDefaultLast30DaysRange()
|
||||
createTimeRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<template>
|
||||
<div class="list-body list-wrap qc-work-order-cards-body">
|
||||
<div v-loading="loading" class="qc-work-order-cards-wrap">
|
||||
<template v-if="useInboundGroups">
|
||||
<template v-if="useGroupedList">
|
||||
<QcWorkOrderSampleGroup
|
||||
v-for="(group, idx) in inboundGroups"
|
||||
v-for="(group, idx) in groupedRows"
|
||||
:key="group.groupKey"
|
||||
:group="group"
|
||||
:group-index="cardIndex(idx)"
|
||||
@@ -63,9 +63,13 @@
|
||||
import { computed } from 'vue'
|
||||
import type { QcWorkOrder } from '@/api/qc-work-order'
|
||||
import type { QcTask } from '@/api/qc-task'
|
||||
import { groupInboundQcWorkOrders } from '@/utils/qc-work-order-groups'
|
||||
import { groupInboundQcWorkOrders, groupOutboundQcWorkOrders } from '@/utils/qc-work-order-groups'
|
||||
import type { QcWorkOrderCardMetaConfig } from '@/utils/qc-work-order-card-meta'
|
||||
import { cardMetaUsesInboundGroups } from '@/utils/qc-work-order-card-meta'
|
||||
import {
|
||||
cardMetaUsesInboundGroups,
|
||||
cardMetaUsesOutboundGroups,
|
||||
cardMetaUsesGroupedList
|
||||
} from '@/utils/qc-work-order-card-meta'
|
||||
import QcWorkOrderSampleGroup from './QcWorkOrderSampleGroup.vue'
|
||||
import QcWorkOrderSingleCard from './QcWorkOrderSingleCard.vue'
|
||||
|
||||
@@ -99,11 +103,17 @@
|
||||
'page-size-change': [size: number]
|
||||
}>()
|
||||
|
||||
const useInboundGroups = computed(() => cardMetaUsesInboundGroups(props.cardMetaConfig.tabKey))
|
||||
const useGroupedList = computed(() => cardMetaUsesGroupedList(props.cardMetaConfig.tabKey))
|
||||
|
||||
const inboundGroups = computed(() =>
|
||||
useInboundGroups.value ? groupInboundQcWorkOrders(props.rows) : []
|
||||
)
|
||||
const groupedRows = computed(() => {
|
||||
if (cardMetaUsesInboundGroups(props.cardMetaConfig.tabKey)) {
|
||||
return groupInboundQcWorkOrders(props.rows)
|
||||
}
|
||||
if (cardMetaUsesOutboundGroups(props.cardMetaConfig.tabKey)) {
|
||||
return groupOutboundQcWorkOrders(props.rows)
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
function cardIndex(i: number) {
|
||||
return (props.page - 1) * props.pageSize + i + 1
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<article class="qc-sample-row">
|
||||
<div class="qc-sample-row__ctx">
|
||||
<span class="qc-sample-row__idx">{{ groupIndex }}</span>
|
||||
<span class="qc-sample-row__code">{{ contextRow.sampleCode || '—' }}</span>
|
||||
<span class="qc-sample-row__code">{{ group.groupTitle || group.sampleCode || '—' }}</span>
|
||||
<span class="qc-sample-row__sub">{{ contextSubtitle }}</span>
|
||||
<span v-if="group.workOrders.length > 1" class="qc-sample-row__badge">
|
||||
{{ group.workOrders.length }} 条质检
|
||||
@@ -13,7 +13,7 @@
|
||||
<div v-if="group.workOrders.length > 1" class="qc-wo-tabs">
|
||||
<div class="qc-wo-tabs__head">
|
||||
<el-icon class="qc-wo-tabs__hint-icon"><Switch /></el-icon>
|
||||
<span class="qc-wo-tabs__hint">该样本有多条质检工单,点击下方切换查看</span>
|
||||
<span class="qc-wo-tabs__hint">{{ groupTabsHint }}</span>
|
||||
</div>
|
||||
<div class="qc-wo-tabs__track" role="tablist" aria-label="同样本质检工单">
|
||||
<button
|
||||
@@ -28,11 +28,7 @@
|
||||
>
|
||||
<span
|
||||
class="qc-wo-tab__source"
|
||||
:class="
|
||||
(wo.triggerSource || 'reception') === 'production'
|
||||
? 'qc-wo-tab__source--production'
|
||||
: 'qc-wo-tab__source--reception'
|
||||
"
|
||||
:class="triggerSourceTabClass(wo.triggerSource)"
|
||||
>
|
||||
<span class="qc-wo-tab__dot"></span>
|
||||
{{ triggerSourceTabLabel(wo) }}
|
||||
@@ -51,7 +47,7 @@
|
||||
|
||||
<QcWorkOrderSingleCard
|
||||
v-if="activeRow"
|
||||
layout="inbound-grouped"
|
||||
layout="grouped"
|
||||
:row="activeRow"
|
||||
:card-meta-config="cardMetaConfig"
|
||||
:task-refresh-token="taskRefreshToken"
|
||||
@@ -76,8 +72,10 @@
|
||||
import type { TagType } from '@/utils/status-tag-registry'
|
||||
import {
|
||||
type QcWorkOrderSampleGroupModel,
|
||||
triggerSourceTabLabel
|
||||
triggerSourceTabLabel,
|
||||
isGateTriggerSource
|
||||
} from '@/utils/qc-work-order-groups'
|
||||
import { getQcWorkOrderTab } from '@/utils/qc-work-order-tabs'
|
||||
import type { QcWorkOrderCardMetaConfig } from '@/utils/qc-work-order-card-meta'
|
||||
import { buildGroupContextSubtitle } from '@/utils/qc-work-order-card-meta'
|
||||
import QcWorkOrderSingleCard from './QcWorkOrderSingleCard.vue'
|
||||
@@ -132,12 +130,27 @@
|
||||
})
|
||||
|
||||
const contextRow = computed(() => {
|
||||
const reception =
|
||||
props.group.workOrders.find((w) => (w.triggerSource || 'reception') === 'reception') ||
|
||||
const gate =
|
||||
props.group.workOrders.find((w) => isGateTriggerSource(w.triggerSource)) ||
|
||||
props.group.workOrders[0]
|
||||
return reception
|
||||
return gate
|
||||
})
|
||||
|
||||
const groupTabsHint = computed(() => {
|
||||
const tab = getQcWorkOrderTab(props.cardMetaConfig.tabKey)
|
||||
if (tab?.listLayout === 'outbound-grouped') {
|
||||
return '该计划有多条质检工单,点击下方切换查看'
|
||||
}
|
||||
return '该样本有多条质检工单,点击下方切换查看'
|
||||
})
|
||||
|
||||
function triggerSourceTabClass(source?: string) {
|
||||
const ts = source || 'reception'
|
||||
if (ts === 'production') return 'qc-wo-tab__source--production'
|
||||
if (ts === 'schedule') return 'qc-wo-tab__source--schedule'
|
||||
return 'qc-wo-tab__source--reception'
|
||||
}
|
||||
|
||||
const contextSubtitle = computed(() => {
|
||||
const row = contextRow.value
|
||||
if (!row) return '—'
|
||||
@@ -247,9 +260,9 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 4px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border: 1px solid #d1fae5;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
background: linear-gradient(180deg, var(--primary-bg, #ecfdf5) 0%, #fff 100%);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -260,8 +273,8 @@
|
||||
min-height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
border: 1px solid transparent;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
@@ -269,13 +282,10 @@
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease;
|
||||
|
||||
&:hover:not(.is-active) {
|
||||
color: #334155;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-color: #dbeafe;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
@@ -284,11 +294,10 @@
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: #065f46;
|
||||
font-weight: 600;
|
||||
background: #fff;
|
||||
border-color: #a7f3d0;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
background: var(--primary, #10b981);
|
||||
box-shadow: 0 1px 3px rgba(16, 185, 129, 0.28);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,6 +316,11 @@
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
&--schedule {
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
&--production {
|
||||
color: #047857;
|
||||
background: #ecfdf5;
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
|
||||
defineOptions({ name: 'QcWorkOrderSingleCard' })
|
||||
|
||||
type CardLayout = 'standalone' | 'inbound-grouped'
|
||||
type CardLayout = 'standalone' | 'inbound-grouped' | 'grouped'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -175,7 +175,10 @@
|
||||
)
|
||||
|
||||
function triggerSourceTagType(source?: string): TagType {
|
||||
return (source || 'reception') === 'production' ? 'warning' : 'primary'
|
||||
const ts = source || 'reception'
|
||||
if (ts === 'production') return 'warning'
|
||||
if (ts === 'schedule') return 'primary'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
function workOrderStatusTagType(status: string): TagType {
|
||||
|
||||
+7
-25
@@ -22,20 +22,6 @@ import { getCardMetaConfig } from '@/utils/qc-work-order-card-meta'
|
||||
|
||||
export type QcWorkOrderTabPageMode = 'manage' | 'mine'
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultLast30DaysRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
export function useQcWorkOrderTabPage(
|
||||
tabKey: Ref<QcWorkOrderTabKey>,
|
||||
pageMode: Ref<QcWorkOrderTabPageMode> | QcWorkOrderTabPageMode
|
||||
@@ -47,13 +33,7 @@ export function useQcWorkOrderTabPage(
|
||||
const filterTimeLabel = computed(() => filterTimeLabelForTab(tabConfig.value))
|
||||
const filterTimeHint = computed(() => {
|
||||
if (pageModeValue.value === 'mine') {
|
||||
const tab = tabConfig.value
|
||||
const timeHint = tabUsesInfusionTimeFilter(tab)
|
||||
? tab.key === 'outbound_basic_experiment'
|
||||
? '默认展示近 30 天内交付的工单。'
|
||||
: '默认展示近 30 天内输注的工单。'
|
||||
: '默认展示近 30 天内创建的工单。'
|
||||
return `仅展示指派给当前登录人的质检任务。${timeHint}`
|
||||
return '仅展示指派给当前登录人的质检任务。'
|
||||
}
|
||||
return filterTimeHintForTab(tabConfig.value)
|
||||
})
|
||||
@@ -65,7 +45,7 @@ export function useQcWorkOrderTabPage(
|
||||
taskStatus: '',
|
||||
taskType: ''
|
||||
})
|
||||
const createTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
|
||||
const createTimeRange = ref<[string, string] | null>(null)
|
||||
const filterExpanded = ref(false)
|
||||
const tableData = ref<QcWorkOrder[]>([])
|
||||
const presetTasksByWorkOrderId = ref<Record<string, QcTask[]>>({})
|
||||
@@ -91,7 +71,9 @@ export function useQcWorkOrderTabPage(
|
||||
keyword: searchForm.keyword || undefined,
|
||||
workOrderStatus: searchForm.workOrderStatus || undefined,
|
||||
triggerSource:
|
||||
tab.group === 'inbound' && searchForm.triggerSource ? searchForm.triggerSource : undefined,
|
||||
tab.listLayout !== 'standalone' && searchForm.triggerSource
|
||||
? searchForm.triggerSource
|
||||
: undefined,
|
||||
tabKey: tabKey.value,
|
||||
...timeParams
|
||||
}
|
||||
@@ -134,7 +116,7 @@ export function useQcWorkOrderTabPage(
|
||||
keyword: searchForm.keyword || undefined,
|
||||
workOrderStatus: searchForm.workOrderStatus || undefined,
|
||||
triggerSource:
|
||||
tab.group === 'inbound' && searchForm.triggerSource
|
||||
tab.listLayout !== 'standalone' && searchForm.triggerSource
|
||||
? searchForm.triggerSource
|
||||
: undefined,
|
||||
...toWorkOrderListQuery(tab),
|
||||
@@ -158,7 +140,7 @@ export function useQcWorkOrderTabPage(
|
||||
searchForm.triggerSource = ''
|
||||
searchForm.taskStatus = ''
|
||||
searchForm.taskType = ''
|
||||
createTimeRange.value = getDefaultLast30DaysRange()
|
||||
createTimeRange.value = null
|
||||
filterExpanded.value = false
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
|
||||
@@ -21,14 +21,19 @@
|
||||
<el-option label="已作废" value="yzf" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="tabConfig.group === 'inbound'" label="触发来源">
|
||||
<el-form-item v-if="tabConfig.listLayout !== 'standalone'" label="触发来源">
|
||||
<el-select
|
||||
v-model="searchForm.triggerSource"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="样本接收" value="reception" />
|
||||
<template v-if="tabConfig.group === 'inbound'">
|
||||
<el-option label="样本接收" value="reception" />
|
||||
</template>
|
||||
<template v-else-if="tabConfig.listLayout === 'outbound-grouped'">
|
||||
<el-option label="计划质检" value="schedule" />
|
||||
</template>
|
||||
<el-option label="生产过程" value="production" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -110,14 +115,19 @@
|
||||
|
||||
<div v-show="filterExpanded" id="my-qc-task-filter-extra" class="filter-extra">
|
||||
<div class="filter-grid my-qc-task-filter-extra-grid">
|
||||
<el-form-item v-if="tabConfig.group === 'inbound'" label="触发来源">
|
||||
<el-form-item v-if="tabConfig.listLayout !== 'standalone'" label="触发来源">
|
||||
<el-select
|
||||
v-model="searchForm.triggerSource"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="样本接收" value="reception" />
|
||||
<template v-if="tabConfig.group === 'inbound'">
|
||||
<el-option label="样本接收" value="reception" />
|
||||
</template>
|
||||
<template v-else-if="tabConfig.listLayout === 'outbound-grouped'">
|
||||
<el-option label="计划质检" value="schedule" />
|
||||
</template>
|
||||
<el-option label="生产过程" value="production" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
@@ -63,8 +63,8 @@
|
||||
<span class="filter-hint">
|
||||
{{
|
||||
activeTab === 'pending'
|
||||
? '勾选多个样本后可批量接收;预计到达时间默认为当天前 15 天至后 15 天。'
|
||||
: '双击卡片可查看接收详情;接收时间默认近 30 天。'
|
||||
? '勾选多个样本后可批量接收。'
|
||||
: '双击卡片可查看接收详情。'
|
||||
}}
|
||||
</span>
|
||||
<div class="filter-actions-end">
|
||||
@@ -748,31 +748,8 @@
|
||||
const activeTab = ref(props.receivedOnly ? 'received' : 'pending')
|
||||
const searchForm = reactive({ keyword: '', sourceType: '' })
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultLast30DaysRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
/** 预计到达时间:当天前 15 天至后 15 天 */
|
||||
function getDefaultEtaDateRange(): [string, string] {
|
||||
const start = new Date()
|
||||
const end = new Date()
|
||||
start.setDate(start.getDate() - 15)
|
||||
end.setDate(end.getDate() + 15)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
const etaDateRange = ref<[string, string] | null>(getDefaultEtaDateRange())
|
||||
const receptionTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
|
||||
const etaDateRange = ref<[string, string] | null>(null)
|
||||
const receptionTimeRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<Record<string, unknown>[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
@@ -1405,9 +1382,9 @@
|
||||
searchForm.keyword = ''
|
||||
searchForm.sourceType = ''
|
||||
if (activeTab.value === 'pending') {
|
||||
etaDateRange.value = getDefaultEtaDateRange()
|
||||
etaDateRange.value = null
|
||||
} else {
|
||||
receptionTimeRange.value = getDefaultLast30DaysRange()
|
||||
receptionTimeRange.value = null
|
||||
}
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
@@ -1581,41 +1558,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin: 0 28px 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
.form-tab-pane {
|
||||
padding: 0 28px 4px;
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span
|
||||
>列表为概要信息,详情见弹窗。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset"
|
||||
@@ -398,26 +398,12 @@
|
||||
}))
|
||||
}
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultCreateTimeRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
/* ====== 列表 ====== */
|
||||
const searchForm = reactive<{ keyword: string; donationStatus: string }>({
|
||||
keyword: '',
|
||||
donationStatus: ''
|
||||
})
|
||||
const createTimeRange = ref<[string, string] | null>(getDefaultCreateTimeRange())
|
||||
const createTimeRange = ref<[string, string] | null>(null)
|
||||
const sampleCollectTimeRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<ResearchDonationRow[]>([])
|
||||
const selectedRows = ref<ResearchDonationRow[]>([])
|
||||
@@ -465,7 +451,7 @@
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.donationStatus = ''
|
||||
createTimeRange.value = getDefaultCreateTimeRange()
|
||||
createTimeRange.value = null
|
||||
sampleCollectTimeRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
|
||||
@@ -1897,42 +1897,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Sticky Tab 导航(与查看弹窗 detail-tabs 一致) ---- */
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin: 0 28px 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
SECTION CARDS
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
@@ -1046,70 +1046,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Sticky Tab 导航 ---- */
|
||||
.detail-tabs {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f0fdf4;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
width: fit-content;
|
||||
isolation: isolate;
|
||||
box-shadow: 0 1px 0 #f0fdf4;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- KPI 摘要条(与采集协议单查看弹窗一致:一行四列 + 薄荷绿通栏) ---- */
|
||||
/* ---- KPI 摘要条(与渠道/申请单查看弹窗一致:浅绿渐变通栏) ---- */
|
||||
.kpi-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0;
|
||||
margin-bottom: 24px;
|
||||
padding: 6px;
|
||||
background: linear-gradient(135deg, #e8f4ee 0%, #f8fafc 52%, #e8f4ee 100%);
|
||||
border: 1px solid #a8d4c4;
|
||||
padding: 18px 20px;
|
||||
background: linear-gradient(180deg, #ecfdf5 0%, #fff 100%);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px 18px;
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid rgba(168, 212, 196, 0.75);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1121,8 +1081,8 @@
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #a8d4c4;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: none;
|
||||
color: #065f46;
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -1208,16 +1168,12 @@
|
||||
&--banner {
|
||||
margin: -22px -24px 18px;
|
||||
padding: 10px 24px;
|
||||
border-bottom: none;
|
||||
border-bottom: 1px solid rgba(16, 185, 129, 0.12);
|
||||
border-radius: 12px 12px 0 0;
|
||||
background: var(--primary, #10b981);
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
|
||||
h3 {
|
||||
color: #fff;
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint"
|
||||
>列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span
|
||||
>列表为概要信息,详情见弹窗。</span
|
||||
>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset"
|
||||
@@ -310,26 +310,12 @@
|
||||
|
||||
defineOptions({ name: 'ResearchDonationList' })
|
||||
|
||||
function formatLocalDate(d: Date): string {
|
||||
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 getDefaultCreateTimeRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - 30)
|
||||
return [formatLocalDate(start), formatLocalDate(end)]
|
||||
}
|
||||
|
||||
/* ====== 列表 ====== */
|
||||
const searchForm = reactive<{ keyword: string; donationStatus: string }>({
|
||||
keyword: '',
|
||||
donationStatus: ''
|
||||
})
|
||||
const createTimeRange = ref<[string, string] | null>(getDefaultCreateTimeRange())
|
||||
const createTimeRange = ref<[string, string] | null>(null)
|
||||
const sampleCollectTimeRange = ref<[string, string] | null>(null)
|
||||
const tableData = ref<ResearchDonationRow[]>([])
|
||||
const selectedRows = ref<ResearchDonationRow[]>([])
|
||||
@@ -368,7 +354,7 @@
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.donationStatus = ''
|
||||
createTimeRange.value = getDefaultCreateTimeRange()
|
||||
createTimeRange.value = null
|
||||
sampleCollectTimeRange.value = null
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
|
||||
@@ -3262,16 +3262,6 @@
|
||||
border-left: 4px solid #047857;
|
||||
}
|
||||
|
||||
.detail-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: #f5f7fa;
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
margin: 8px 22px 16px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.order-tab-pane {
|
||||
margin-top: 6px;
|
||||
padding: 0 22px 8px;
|
||||
@@ -3297,26 +3287,6 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.detail-tab {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: #fff;
|
||||
color: #065f46;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: #065f46;
|
||||
}
|
||||
}
|
||||
|
||||
.lock-tip {
|
||||
background: #fff1f0;
|
||||
border: 1px solid #ffccc7;
|
||||
|
||||
Reference in new issue
Block a user