This commit is contained in:
zlwu committed 2026-05-25 15:14:06 +08:00
1 parent 575e63e7e8
commit 771f010a39
165 files changed
+67359 -21698

No files matched your search

@@ -22,9 +22,20 @@
clearable
/>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持模糊查询。</span>
<span class="filter-hint">支持模糊查询;默认展示近 30 天内创建的客户。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
@@ -239,7 +250,22 @@
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 tableData = ref<Api.BizCustomer.CustomerItem[]>([])
const selectedRows = ref<Api.BizCustomer.CustomerItem[]>([])
const loading = ref(false)
@@ -303,7 +329,9 @@
const res = await fetchCustomerList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined
keyword: searchForm.keyword || undefined,
createTimeStart: createTimeRange.value?.[0] || undefined,
createTimeEnd: createTimeRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -319,6 +347,7 @@
function handleReset() {
searchForm.keyword = ''
createTimeRange.value = getDefaultCreateTimeRange()
pagination.page = 1
loadData()
}
@@ -412,7 +441,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, createTimeRange],
() => {
pagination.page = 1
loadData()
@@ -37,8 +37,20 @@
<el-option label="已办保险" value="1" />
</el-select>
</el-form-item>
<el-form-item label="满月日期">
<el-date-picker
v-model="fullmoonDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按协议号、姓名模糊检索;待办状态默认未办保险,满月日期默认近 30 天。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -481,10 +493,25 @@
defineOptions({ name: 'CellStorageFullmoonTodo' })
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: '' })
const searchForm = reactive({ keyword: '', todoStatus: '0' })
const fullmoonDateRange = ref<[string, string] | null>(getDefaultFullmoonDateRange())
const tableData = ref<any[]>([])
const tableRef = ref()
const selectedRows = ref<any[]>([])
@@ -615,7 +642,9 @@
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
todoStatus: searchForm.todoStatus || undefined
todoStatus: searchForm.todoStatus || undefined,
fullmoonDateStart: fullmoonDateRange.value?.[0] || undefined,
fullmoonDateEnd: fullmoonDateRange.value?.[1] || undefined
})
tableData.value = (res as any)?.list || []
pagination.total = (res as any)?.total ?? 0
@@ -631,7 +660,8 @@
function handleReset() {
searchForm.keyword = ''
searchForm.todoStatus = ''
searchForm.todoStatus = '0'
fullmoonDateRange.value = getDefaultFullmoonDateRange()
pagination.page = 1
loadData()
}
@@ -1093,7 +1123,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, fullmoonDateRange],
() => {
pagination.page = 1
loadData()
@@ -30,6 +30,45 @@
<h2>缴费确认列表</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" label-position="top" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="协议号/客户姓名/身份证号/新生儿姓名/新生儿身份证号"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="应缴费时间">
<el-date-picker
v-model="installmentDueDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint"
>支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示近 30 天内应缴费的明细。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-body list-wrap payment-confirm-table-wrap">
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id" @row-dblclick="handleRowDblClick">
<el-table-column
@@ -230,7 +269,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Close, Check } from '@element-plus/icons-vue'
import { Close, Check, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
fetchReceiptConfirmList,
fetchReceiptConfirmedList,
@@ -256,6 +295,23 @@
defineOptions({ name: 'CellStoragePaymentConfirm' })
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 end = new Date()
const start = new Date()
start.setDate(start.getDate() - 30)
return [formatLocalDate(start), formatLocalDate(end)]
}
const searchForm = reactive({ keyword: '' })
const installmentDueDateRange = ref<[string, string] | null>(getDefaultInstallmentDueDateRange())
const tabs = [
{ key: 'pending', label: '待确认' },
{ key: 'confirmed', label: '已确认' }
@@ -311,10 +367,20 @@
downloadAttachmentFile(file, { fullFileUrl })
}
function buildListParams() {
return {
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
installmentDueDateStart: installmentDueDateRange.value?.[0] || undefined,
installmentDueDateEnd: installmentDueDateRange.value?.[1] || undefined
}
}
async function loadData() {
loading.value = true
try {
const params = { page: pagination.page, pageSize: pagination.pageSize }
const params = buildListParams()
const res =
activeTab.value === 'pending'
? await fetchReceiptConfirmList(params)
@@ -333,6 +399,18 @@
loadData()
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
installmentDueDateRange.value = getDefaultInstallmentDueDateRange()
pagination.page = 1
loadData()
}
function openConfirm(row: Record<string, unknown>) {
currentDetailId.value = String(row.id)
confirmRow.value = row
@@ -370,6 +448,15 @@
}
}
watchDebounced(
[searchForm, installmentDueDateRange],
() => {
pagination.page = 1
loadData()
},
{ debounce: 300, deep: true }
)
onMounted(() => {
loadData()
})
@@ -404,6 +491,19 @@
width: 100%;
}
.filter-form :deep(.el-form-item) {
margin-bottom: 0;
min-width: 0;
}
.filter-form :deep(.el-form-item__label) {
font-size: 13px;
font-weight: 500;
color: var(--mint-text-muted, #64748b);
padding: 0 0 4px;
line-height: 1.2;
}
.payment-confirm-table-wrap {
overflow-x: auto;
}
@@ -48,9 +48,20 @@
<el-option label="现金" value="xj" />
</el-select>
</el-form-item>
<el-form-item label="应缴费时间">
<el-date-picker
v-model="installmentDueDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按协议号模糊检索。</span>
<span class="filter-hint">支持按协议号模糊检索;默认展示近 30 天内应缴费的明细。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -447,6 +458,20 @@
defineOptions({ name: 'CellStoragePaymentInfo' })
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 end = new Date()
const start = new Date()
start.setDate(start.getDate() - 30)
return [formatLocalDate(start), formatLocalDate(end)]
}
const tabs = [
{ key: 'dfj', label: '待缴费' },
{ key: 'jfqrz', label: '缴费确认中' },
@@ -457,6 +482,7 @@
const activeTab = ref('dfj')
const searchForm = reactive({ keyword: '', transferType: '' })
const installmentDueDateRange = ref<[string, string] | null>(getDefaultInstallmentDueDateRange())
const tableData = ref<Record<string, unknown>[]>([])
const selectedRows = ref<Record<string, unknown>[]>([])
const loading = ref(false)
@@ -533,7 +559,9 @@
pageSize: pagination.pageSize,
payStatus: activeTab.value,
keyword: searchForm.keyword || undefined,
transferType: searchForm.transferType || undefined
transferType: searchForm.transferType || undefined,
installmentDueDateStart: installmentDueDateRange.value?.[0] || undefined,
installmentDueDateEnd: installmentDueDateRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -713,12 +741,13 @@
function handleReset() {
searchForm.keyword = ''
searchForm.transferType = ''
installmentDueDateRange.value = getDefaultInstallmentDueDateRange()
pagination.page = 1
loadData()
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, installmentDueDateRange],
() => {
pagination.page = 1
loadData()
@@ -32,9 +32,32 @@
<el-option label="转科研" value="zky" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="样本采集时间">
<el-date-picker
v-model="sampleCollectTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">列表为概要信息,详情见弹窗。</span>
<span class="filter-hint">列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -326,11 +349,27 @@
defineOptions({ name: 'CellStorageProtocolAudit' })
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 sampleCollectTimeRange = ref<[string, string] | null>(null)
const tableData = ref<CollectionProtocolRow[]>([])
const selectedRows = ref<CollectionProtocolRow[]>([])
const loading = ref(false)
@@ -347,7 +386,11 @@
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
protocolStatus: searchForm.protocolStatus || undefined
protocolStatus: searchForm.protocolStatus || undefined,
createTimeStart: createTimeRange.value?.[0] || undefined,
createTimeEnd: createTimeRange.value?.[1] || undefined,
sampleCollectTimeStart: sampleCollectTimeRange.value?.[0] || undefined,
sampleCollectTimeEnd: sampleCollectTimeRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -364,6 +407,8 @@
function handleReset() {
searchForm.keyword = ''
searchForm.protocolStatus = ''
createTimeRange.value = getDefaultCreateTimeRange()
sampleCollectTimeRange.value = null
pagination.page = 1
loadData()
}
@@ -485,7 +530,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, createTimeRange, sampleCollectTimeRange],
() => {
pagination.page = 1
loadData()
@@ -32,9 +32,32 @@
<el-option label="转科研" value="zky" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="样本采集时间">
<el-date-picker
v-model="sampleCollectTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">列表为概要信息,详情见弹窗。</span>
<span class="filter-hint">列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -284,11 +307,27 @@
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 sampleCollectTimeRange = ref<[string, string] | null>(null)
const tableData = ref<CollectionProtocolRow[]>([])
const selectedRows = ref<CollectionProtocolRow[]>([])
const loading = ref(false)
@@ -305,7 +344,11 @@
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
protocolStatus: searchForm.protocolStatus || undefined
protocolStatus: searchForm.protocolStatus || undefined,
createTimeStart: createTimeRange.value?.[0] || undefined,
createTimeEnd: createTimeRange.value?.[1] || undefined,
sampleCollectTimeStart: sampleCollectTimeRange.value?.[0] || undefined,
sampleCollectTimeEnd: sampleCollectTimeRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -322,6 +365,8 @@
function handleReset() {
searchForm.keyword = ''
searchForm.protocolStatus = ''
createTimeRange.value = getDefaultCreateTimeRange()
sampleCollectTimeRange.value = null
pagination.page = 1
loadData()
}
@@ -391,7 +436,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, createTimeRange, sampleCollectTimeRange],
() => {
pagination.page = 1
loadData()
@@ -30,6 +30,45 @@
<h2>退费确认列表</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" label-position="top" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="协议号/客户姓名/身份证号/新生儿姓名/新生儿身份证号"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="应缴费时间">
<el-date-picker
v-model="installmentDueDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint"
>支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示近 30 天内应缴费的明细。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-body list-wrap refund-confirm-table-wrap">
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id" @row-dblclick="handleRowDblClick">
<el-table-column
@@ -247,7 +286,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Close, Check } from '@element-plus/icons-vue'
import { Close, Check, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
fetchRefundConfirmList,
fetchRefundConfirmedList,
@@ -273,6 +312,23 @@
defineOptions({ name: 'CellStorageRefundConfirm' })
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 end = new Date()
const start = new Date()
start.setDate(start.getDate() - 30)
return [formatLocalDate(start), formatLocalDate(end)]
}
const searchForm = reactive({ keyword: '' })
const installmentDueDateRange = ref<[string, string] | null>(getDefaultInstallmentDueDateRange())
const tabs = [
{ key: 'pending', label: '待确认' },
{ key: 'confirmed', label: '已确认' }
@@ -328,10 +384,20 @@
downloadAttachmentFile(file, { fullFileUrl })
}
function buildListParams() {
return {
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
installmentDueDateStart: installmentDueDateRange.value?.[0] || undefined,
installmentDueDateEnd: installmentDueDateRange.value?.[1] || undefined
}
}
async function loadData() {
loading.value = true
try {
const params = { page: pagination.page, pageSize: pagination.pageSize }
const params = buildListParams()
const res =
activeTab.value === 'pending'
? await fetchRefundConfirmList(params)
@@ -350,6 +416,18 @@
loadData()
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
installmentDueDateRange.value = getDefaultInstallmentDueDateRange()
pagination.page = 1
loadData()
}
function openConfirm(row: Record<string, unknown>) {
currentDetailId.value = String(row.id)
confirmRow.value = row
@@ -387,6 +465,15 @@
}
}
watchDebounced(
[searchForm, installmentDueDateRange],
() => {
pagination.page = 1
loadData()
},
{ debounce: 300, deep: true }
)
onMounted(() => {
loadData()
})
@@ -421,6 +508,19 @@
width: 100%;
}
.filter-form :deep(.el-form-item) {
margin-bottom: 0;
min-width: 0;
}
.filter-form :deep(.el-form-item__label) {
font-size: 13px;
font-weight: 500;
color: var(--mint-text-muted, #64748b);
padding: 0 0 4px;
line-height: 1.2;
}
.refund-confirm-table-wrap {
overflow-x: auto;
}
@@ -30,8 +30,20 @@
<el-option label="未通知" value="0" />
</el-select>
</el-form-item>
<el-form-item label="接收时间">
<el-date-picker
v-model="receptionTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按协议号、姓名、样本编码模糊检索;默认展示近 30 天内接收的样本。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -155,7 +167,22 @@
defineOptions({ name: 'CellStorageSampleReceptionInfo' })
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 tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
@@ -174,7 +201,9 @@
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
acceptConclusion: searchForm.acceptConclusion || undefined,
smsNotified: searchForm.smsNotified || undefined
smsNotified: searchForm.smsNotified || undefined,
receptionTimeStart: receptionTimeRange.value?.[0] || undefined,
receptionTimeEnd: receptionTimeRange.value?.[1] || undefined
})
const res = (result as any) || {}
tableData.value = res.list || []
@@ -191,6 +220,7 @@
function handleReset() {
Object.assign(searchForm, { keyword: '', acceptConclusion: '', smsNotified: '' })
receptionTimeRange.value = getDefaultReceptionTimeRange()
handleSearch()
}
@@ -240,7 +270,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, receptionTimeRange],
() => {
pagination.page = 1
loadData()
@@ -24,8 +24,20 @@
<el-option label="已制作" value="1" />
</el-select>
</el-form-item>
<el-form-item label="入库日期">
<el-date-picker
v-model="storageInDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按协议号、姓名模糊检索;制作状态默认未制作,入库日期默认近 30 天。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -317,10 +329,25 @@
defineOptions({ name: 'CellStorageStorageCert' })
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: '' })
const searchForm = reactive({ keyword: '', makeStatus: '0' })
const storageInDateRange = ref<[string, string] | null>(getDefaultStorageInDateRange())
const tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
@@ -393,7 +420,9 @@
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
makeStatus: searchForm.makeStatus || undefined
makeStatus: searchForm.makeStatus || undefined,
storageInDateStart: storageInDateRange.value?.[0] || undefined,
storageInDateEnd: storageInDateRange.value?.[1] || undefined
})
tableData.value = (res as any)?.list || []
pagination.total = (res as any)?.total ?? 0
@@ -409,7 +438,8 @@
function handleReset() {
searchForm.keyword = ''
searchForm.makeStatus = ''
searchForm.makeStatus = '0'
storageInDateRange.value = getDefaultStorageInDateRange()
pagination.page = 1
loadData()
}
@@ -593,7 +623,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, storageInDateRange],
() => {
pagination.page = 1
loadData()
@@ -30,8 +30,20 @@
<el-option label="未通知" value="0" />
</el-select>
</el-form-item>
<el-form-item label="入库时间">
<el-date-picker
v-model="storageTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按协议号、姓名、样本编码模糊检索;默认展示近 30 天内入库的样本。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -161,7 +173,22 @@
defineOptions({ name: 'CellStorageStorageEntryInfo' })
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 tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
@@ -180,7 +207,9 @@
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
storageStatus: searchForm.storageStatus || undefined,
smsNotified: searchForm.smsNotified || undefined
smsNotified: searchForm.smsNotified || undefined,
storageTimeStart: storageTimeRange.value?.[0] || undefined,
storageTimeEnd: storageTimeRange.value?.[1] || undefined
})
const res = (result as any) || {}
tableData.value = res.list || []
@@ -197,6 +226,7 @@
function handleReset() {
Object.assign(searchForm, { keyword: '', storageStatus: '', smsNotified: '' })
storageTimeRange.value = getDefaultStorageTimeRange()
handleSearch()
}
@@ -246,7 +276,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, storageTimeRange],
() => {
pagination.page = 1
loadData()
@@ -18,9 +18,20 @@
<el-form-item label="关键词">
<el-input v-model="searchForm.keyword" placeholder="预约编号/来访单位/联系人" clearable />
</el-form-item>
<el-form-item label="到访日期">
<el-date-picker
v-model="visitDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">按预约编号、来访单位、联系人模糊查询。</span>
<span class="filter-hint">按预约编号、来访单位、联系人模糊查询;默认展示未来 30 天内的到访预约。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
@@ -190,7 +201,22 @@
[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 tableData = ref<AppointmentRow[]>([])
const selectedRows = ref<AppointmentRow[]>([])
const loading = ref(false)
@@ -245,7 +271,9 @@
const res = await fetchExhibitionAppointmentList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined
keyword: searchForm.keyword || undefined,
visitDateStart: visitDateRange.value?.[0] || undefined,
visitDateEnd: visitDateRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -261,6 +289,7 @@
function handleReset() {
searchForm.keyword = ''
visitDateRange.value = getDefaultVisitDateRange()
pagination.page = 1
loadData()
}
@@ -350,7 +379,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, visitDateRange],
() => {
pagination.page = 1
loadData()
@@ -37,9 +37,20 @@
<el-option label="退订产品" value="unsubscribe" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint"></span>
<span class="filter-hint">默认展示近 30 天内创建的流水。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -391,7 +402,22 @@
done: ['yqr']
}
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 activeTab = ref('pending')
const createTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
const searchForm = reactive({
txnType: '' as string,
txnStatus: ['dqr'] as string[],
@@ -418,6 +444,8 @@
params.txnStatus = searchForm.txnStatus.join(',')
}
if (searchForm.keyword.trim()) params.keyword = searchForm.keyword.trim()
if (createTimeRange.value?.[0]) params.createTimeStart = createTimeRange.value[0]
if (createTimeRange.value?.[1]) params.createTimeEnd = createTimeRange.value[1]
const res: any = await fetchServiceOrderTxnList(params)
tableData.value = res?.data?.list || res?.list || []
@@ -438,6 +466,7 @@
searchForm.txnType = ''
searchForm.txnStatus = [...TAB_STATUS_MAP[activeTab.value]]
searchForm.keyword = ''
createTimeRange.value = getDefaultLast30DaysRange()
pagination.page = 1
loadData()
}
@@ -453,7 +482,7 @@
// 检索条件变更时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, createTimeRange],
() => {
pagination.page = 1
loadData()
@@ -45,9 +45,20 @@
<el-option label="已作废" value="yzf" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">渠道名称输入后自动检索;订单类型、状态变更后亦会自动刷新列表。</span>
<span class="filter-hint">默认展示近 30 天内创建的订单;渠道名称输入后自动检索。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -728,6 +739,22 @@ const formatProjectOptionLabel = (p: 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 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 searchForm = reactive<{
orderType: string
orderStatus: string[]
@@ -758,6 +785,8 @@ const loadData = async () => {
if (searchForm.orderType) params.orderType = searchForm.orderType
if (searchForm.orderStatus.length) params.orderStatus = searchForm.orderStatus.join(',')
if (searchForm.keyword) params.keyword = searchForm.keyword
if (createTimeRange.value?.[0]) params.createTimeStart = createTimeRange.value[0]
if (createTimeRange.value?.[1]) params.createTimeEnd = createTimeRange.value[1]
const res: any = await fetchChannelServiceOrderList(params)
const data = res?.data ?? res
@@ -779,13 +808,14 @@ const handleReset = () => {
searchForm.orderType = ''
searchForm.orderStatus = []
searchForm.keyword = ''
createTimeRange.value = getDefaultLast30DaysRange()
pagination.page = 1
loadData()
}
// 检索条件变更时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, createTimeRange],
() => {
pagination.page = 1
loadData()
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,651 @@
<!-- 中试服务申请单列表页 -->
<template>
<div class="page-container">
<header class="page-head">
<h1>中试服务申请单</h1>
<p class="page-desc">管理中试服务申请单的创建、审批、状态流转,关联渠道服务订单与患者信息。</p>
</header>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">中试服务申请单</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="申请单号">
<el-input
v-model="searchForm.applicationNo"
placeholder="请输入申请单号"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="渠道">
<el-select
v-model="searchForm.channelId"
placeholder="全部"
clearable
filterable
:loading="channelLoading"
@focus="onChannelSelectFocus"
@change="handleSearchChannelChange"
>
<el-option
v-for="ch in channelOptions"
:key="ch.id"
:label="ch.channelName || ch.name"
:value="ch.id"
/>
</el-select>
</el-form-item>
<el-form-item label="服务订单">
<el-select
v-model="searchForm.channelServiceOrderId"
placeholder="全部"
clearable
filterable
:disabled="!searchForm.channelId"
:loading="serviceOrderLoading"
@change="handleSearch"
>
<el-option
v-for="so in serviceOrderOptions"
:key="so.id"
:label="formatServiceOrderLabel(so)"
:value="so.id"
/>
</el-select>
</el-form-item>
</div>
<div class="filter-actions-row">
<div class="status-filter-group">
<el-button
v-for="item in statusFilters"
:key="item.value"
:type="activeStatus === item.value ? 'primary' : 'default'"
size="default"
@click="handleStatusFilter(item.value)"
>
{{ item.label }}
</el-button>
</div>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<el-button class="btn-main" type="primary" @click="handleCreate">
<el-icon><Plus /></el-icon>
新增
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@row-dblclick="handleView"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="applicationNo" label="申请单号" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<el-button link type="primary" @click="handleView(row)">
{{ row.applicationNo || '-' }}
</el-button>
</template>
</el-table-column>
<el-table-column prop="channelName" label="渠道名称" min-width="150" show-overflow-tooltip />
<el-table-column label="关联服务订单" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.serviceOrderNo">{{ row.serviceOrderNo }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="patientName" label="患者姓名" min-width="100" show-overflow-tooltip>
<template #default="{ row }">{{ row.patientName || '-' }}</template>
</el-table-column>
<el-table-column prop="infusionPlanCount" label="输注方案数" min-width="100" align="center">
<template #default="{ row }">{{ row.infusionPlanCount ?? 0 }}</template>
</el-table-column>
<el-table-column prop="applicationStatus" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="applicationStatusTagType(row.applicationStatus)" size="small">
{{ applicationStatusLabel(row.applicationStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="160" align="center" />
<el-table-column label="操作" min-width="280" fixed="right" align="center" header-align="center">
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleView(row)">查看</el-button>
<el-button
v-if="row.applicationStatus === 'dks'"
class="btn-op-item"
@click="handleEdit(row)"
>
编辑
</el-button>
<el-popconfirm
v-if="row.applicationStatus === 'dks'"
title="确定删除该申请单吗?"
confirm-button-text="确定"
cancel-button-text="取消"
@confirm="handleDelete(row)"
>
<template #reference>
<el-button class="btn-op-item op-warn">删除</el-button>
</template>
</el-popconfirm>
<el-button
v-if="row.applicationStatus === 'dks'"
class="btn-op-item primary"
type="primary"
@click="handleStart(row)"
>
开始
</el-button>
<el-button
v-if="row.applicationStatus === 'dks'"
class="btn-op-item op-warn"
type="danger"
@click="handleVoid(row)"
>
作废
</el-button>
<el-button
v-if="row.applicationStatus === 'jxz'"
class="btn-op-item primary"
type="primary"
@click="handleComplete(row)"
>
完成
</el-button>
<el-button
v-if="row.applicationStatus === 'jxz'"
class="btn-op-item op-warn"
type="danger"
@click="handleVoid(row)"
>
作废
</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, 100]"
layout="total, sizes, prev, pager, next, jumper"
@update:current-page="val => (pagination.page = val)"
@update:page-size="val => (pagination.pageSize = val)"
@size-change="loadData"
@current-change="loadData"
/>
</div>
</div>
</section>
<!-- 新增申请单弹窗 -->
<el-dialog
v-model="dialogVisible"
title="新增中试服务申请单"
width="600px"
destroy-on-close
:close-on-click-modal="false"
@close="handleDialogClose"
>
<div class="dialog-form-layout">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="dialog-form"
>
<div class="form-grid-2">
<el-form-item label="渠道" prop="channelId">
<el-select
v-model="formData.channelId"
placeholder="请选择渠道"
filterable
clearable
:loading="dialogChannelLoading"
@focus="onDialogChannelFocus"
@change="handleDialogChannelChange"
>
<el-option
v-for="ch in dialogChannelOptions"
:key="ch.id"
:label="ch.channelName || ch.name"
:value="ch.id"
/>
</el-select>
</el-form-item>
<el-form-item label="服务订单" prop="channelServiceOrderId">
<el-select
v-model="formData.channelServiceOrderId"
placeholder="请先选择渠道,再选择服务订单"
filterable
clearable
:disabled="!formData.channelId"
:loading="dialogServiceOrderLoading"
>
<el-option
v-for="so in dialogServiceOrderOptions"
:key="so.id"
:label="formatServiceOrderLabel(so)"
:value="so.id"
/>
</el-select>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input
v-model="formData.remark"
type="textarea"
placeholder="请输入备注"
:rows="3"
/>
</el-form-item>
</div>
</el-form>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit">
<el-icon><Check /></el-icon>确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import {
Plus,
Search,
RefreshLeft,
Close,
Check,
} from '@element-plus/icons-vue'
import {
fetchPilotApplicationList,
createPilotApplication,
deletePilotApplication,
startPilotApplication,
completePilotApplication,
voidPilotApplication,
fetchChannelList,
fetchChannelServiceOrderList,
} from '@/api/admin'
import {
applicationStatusLabel,
applicationStatusTagType,
channelServiceOrderTypeLabel,
} from '@/utils/status-tag-registry'
defineOptions({ name: 'PilotApplicationManagement' })
// ── 工具函数 ──────────────────────────────────────────────────────────
const displayValue = (v: any): string => {
if (v == null || v === '' || v === undefined) return '-'
return String(v)
}
const formatServiceOrderLabel = (so: any) => {
const no = so.orderNo || so.id || ''
const typeLabel = channelServiceOrderTypeLabel(so.orderType)
return `${no}(${typeLabel})`
}
const unwrapApiList = (res: any): any[] => {
const d = res?.data ?? res
if (Array.isArray(d)) return d
return d?.list ?? d?.records ?? []
}
// ── 状态筛选 ──────────────────────────────────────────────────────────
const statusFilters = [
{ label: '全部', value: '' },
{ label: '待开始', value: 'dks' },
{ label: '进行中', value: 'jxz' },
{ label: '已完成', value: 'ywc' },
{ label: '已作废', value: 'yzf' },
]
const activeStatus = ref('')
const handleStatusFilter = (val: string) => {
activeStatus.value = val
handleSearch()
}
// ── 搜索 ──────────────────────────────────────────────────────────────
const searchForm = reactive({
applicationNo: '',
channelId: '',
channelServiceOrderId: '',
})
// ── 搜索区域渠道下拉 ──────────────────────────────────────────────────
const channelOptions = ref<any[]>([])
const channelLoading = ref(false)
const channelLoaded = ref(false)
const onChannelSelectFocus = () => {
if (!channelLoaded.value) {
loadChannelOptions()
}
}
const loadChannelOptions = async () => {
channelLoading.value = true
try {
const res: any = await fetchChannelList({ page: 1, pageSize: 200 })
channelOptions.value = unwrapApiList(res)
channelLoaded.value = true
} catch {
channelOptions.value = []
} finally {
channelLoading.value = false
}
}
// ── 搜索区域服务订单下拉(联动渠道) ──────────────────────────────────
const serviceOrderOptions = ref<any[]>([])
const serviceOrderLoading = ref(false)
const handleSearchChannelChange = () => {
searchForm.channelServiceOrderId = ''
serviceOrderOptions.value = []
if (searchForm.channelId) {
loadSearchServiceOrders(searchForm.channelId)
}
handleSearch()
}
const loadSearchServiceOrders = async (channelId: string) => {
serviceOrderLoading.value = true
try {
const res: any = await fetchChannelServiceOrderList({
channelId,
page: 1,
pageSize: 200,
})
serviceOrderOptions.value = unwrapApiList(res)
} catch {
serviceOrderOptions.value = []
} finally {
serviceOrderLoading.value = false
}
}
// ── 表格 ──────────────────────────────────────────────────────────────
const tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const loadData = async () => {
loading.value = true
try {
const params: Record<string, any> = {
page: pagination.page,
pageSize: pagination.pageSize,
}
if (searchForm.applicationNo) params.applicationNo = searchForm.applicationNo
if (searchForm.channelId) params.channelId = searchForm.channelId
if (searchForm.channelServiceOrderId) params.channelServiceOrderId = searchForm.channelServiceOrderId
if (activeStatus.value) params.applicationStatus = activeStatus.value
const res: any = await fetchPilotApplicationList(params)
const data = res?.data ?? res
tableData.value = data?.list ?? data?.records ?? []
pagination.total = data?.total ?? 0
} catch (e) {
console.error('加载申请单列表失败:', e)
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.page = 1
loadData()
}
const handleReset = () => {
searchForm.applicationNo = ''
searchForm.channelId = ''
searchForm.channelServiceOrderId = ''
activeStatus.value = ''
serviceOrderOptions.value = []
pagination.page = 1
loadData()
}
// ── 查看详情 ──────────────────────────────────────────────────────────
const router = useRouter()
const handleView = (row: any) => {
router.push({ name: 'PilotApplicationDetail', query: { id: row.id } })
}
// ── 新增弹窗 ──────────────────────────────────────────────────────────
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
const initFormData = () => ({
channelId: '',
channelServiceOrderId: '',
remark: '',
})
const formData = reactive(initFormData())
const formRules: FormRules = {
channelId: [{ required: true, message: '请选择渠道', trigger: 'change' }],
channelServiceOrderId: [{ required: true, message: '请选择服务订单', trigger: 'change' }],
}
// ── 弹窗渠道下拉 ──────────────────────────────────────────────────────
const dialogChannelOptions = ref<any[]>([])
const dialogChannelLoading = ref(false)
const dialogChannelLoaded = ref(false)
const onDialogChannelFocus = () => {
if (!dialogChannelLoaded.value) {
loadDialogChannelOptions()
}
}
const loadDialogChannelOptions = async () => {
dialogChannelLoading.value = true
try {
const res: any = await fetchChannelList({ page: 1, pageSize: 200 })
dialogChannelOptions.value = unwrapApiList(res)
dialogChannelLoaded.value = true
} catch {
dialogChannelOptions.value = []
} finally {
dialogChannelLoading.value = false
}
}
// ── 弹窗服务订单下拉(联动渠道) ──────────────────────────────────────
const dialogServiceOrderOptions = ref<any[]>([])
const dialogServiceOrderLoading = ref(false)
const handleDialogChannelChange = async (channelId: string) => {
formData.channelServiceOrderId = ''
dialogServiceOrderOptions.value = []
if (!channelId) return
dialogServiceOrderLoading.value = true
try {
const res: any = await fetchChannelServiceOrderList({
channelId,
page: 1,
pageSize: 200,
})
dialogServiceOrderOptions.value = unwrapApiList(res)
} catch {
dialogServiceOrderOptions.value = []
} finally {
dialogServiceOrderLoading.value = false
}
}
const handleCreate = () => {
Object.assign(formData, initFormData())
dialogChannelLoaded.value = false
dialogChannelOptions.value = []
dialogServiceOrderOptions.value = []
dialogVisible.value = true
void loadDialogChannelOptions()
}
const handleDialogClose = () => {
formRef.value?.resetFields()
}
const handleSubmit = async () => {
try {
await formRef.value?.validate()
} catch {
return
}
submitLoading.value = true
try {
const res: any = await createPilotApplication({
channelServiceOrderId: formData.channelServiceOrderId,
remark: formData.remark,
})
dialogVisible.value = false
ElMessage.success('创建成功')
// 跳转详情页
const data = res?.data ?? res
const newId = data?.id || data?.pilotApplicationId || data
if (newId) {
router.push({ name: 'PilotApplicationDetail', query: { id: newId } })
} else {
loadData()
}
} catch (e: any) {
ElMessage.error(e?.message || '创建失败')
} finally {
submitLoading.value = false
}
}
// ── 编辑 ──────────────────────────────────────────────────────────────
const handleEdit = (row: any) => {
// Phase 1 编辑仅修改备注,直接跳详情页
router.push({ name: 'PilotApplicationDetail', query: { id: row.id } })
}
// ── 删除 ──────────────────────────────────────────────────────────────
const handleDelete = async (row: any) => {
try {
await deletePilotApplication({ id: row.id })
ElMessage.success('删除成功')
loadData()
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
}
}
// ── 状态操作 ──────────────────────────────────────────────────────────
const handleStart = async (row: any) => {
try {
await ElMessageBox.confirm('确定开始该申请单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
})
await startPilotApplication({ id: row.id })
ElMessage.success('操作成功')
loadData()
} catch (e: any) {
if (e !== 'cancel') {
ElMessage.error(e?.message || '操作失败')
}
}
}
const handleComplete = async (row: any) => {
try {
await ElMessageBox.confirm('确定完成该申请单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
})
await completePilotApplication({ id: row.id })
ElMessage.success('操作成功')
loadData()
} catch (e: any) {
if (e !== 'cancel') {
ElMessage.error(e?.message || '操作失败')
}
}
}
const handleVoid = async (row: any) => {
try {
await ElMessageBox.confirm('确定作废该申请单吗?作废后不可恢复。', '提示', {
confirmButtonText: '确定作废',
cancelButtonText: '取消',
type: 'warning',
})
await voidPilotApplication({ id: row.id })
ElMessage.success('操作成功')
loadData()
} catch (e: any) {
if (e !== 'cancel') {
ElMessage.error(e?.message || '操作失败')
}
}
}
// ── 初始化 ────────────────────────────────────────────────────────────
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.status-filter-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
</style>
@@ -24,8 +24,20 @@
<el-option label="已完成" value="ywc" />
</el-select>
</el-form-item>
<el-form-item label="开始时间">
<el-date-picker
v-model="startedTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">默认展示近 30 天内已开始的任务;清空日期可查看尚未开始的待配置任务。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
@@ -325,7 +337,22 @@
// ---------------------------------------------------------------------------
// 列表视图
// ---------------------------------------------------------------------------
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 searchForm = reactive({ keyword: '', taskStatus: '' })
const startedTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
const tableData = ref<Record<string, any>[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
@@ -344,7 +371,9 @@
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
taskStatus: searchForm.taskStatus || undefined
taskStatus: searchForm.taskStatus || undefined,
startedTimeStart: startedTimeRange.value?.[0] || undefined,
startedTimeEnd: startedTimeRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -361,6 +390,7 @@
function handleReset() {
searchForm.keyword = ''
searchForm.taskStatus = ''
startedTimeRange.value = getDefaultLast30DaysRange()
pagination.page = 1
loadData()
}
@@ -400,7 +430,7 @@
// ---------------------------------------------------------------------------
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, startedTimeRange],
() => {
pagination.page = 1
loadData()
@@ -40,8 +40,20 @@
<el-option label="已完成" value="ywc" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">默认展示近 30 天内创建的工单。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -519,7 +531,22 @@
return map[source] || { label: source, type: 'info' }
}
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 searchForm = reactive({ keyword: '', workOrderStatus: '' })
const createTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
const tableData = ref<any[]>([])
const workOrderListTableRef = ref<TableInstance>()
const loading = ref(false)
@@ -544,7 +571,9 @@
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
workOrderStatus: searchForm.workOrderStatus || undefined,
workOrderType: activeTypeTab.value === 'all' ? undefined : activeTypeTab.value
workOrderType: activeTypeTab.value === 'all' ? undefined : activeTypeTab.value,
createTimeStart: createTimeRange.value?.[0] || undefined,
createTimeEnd: createTimeRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -560,6 +589,7 @@
function handleReset() {
searchForm.keyword = ''
searchForm.workOrderStatus = ''
createTimeRange.value = getDefaultLast30DaysRange()
pagination.page = 1
loadData()
}
@@ -782,7 +812,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, createTimeRange],
() => {
pagination.page = 1
loadData()
@@ -35,8 +35,20 @@
<el-option label="已完成" value="ywc" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">默认展示近 30 天内创建的工单。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
@@ -575,7 +587,22 @@
}
}
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 searchForm = reactive({ keyword: '', workOrderStatus: '' })
const createTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
const tableData = ref<QcWorkOrder[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
@@ -929,7 +956,9 @@
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
workOrderStatus: searchForm.workOrderStatus || undefined,
workOrderType: activeTypeTab.value === 'all' ? undefined : activeTypeTab.value
workOrderType: activeTypeTab.value === 'all' ? undefined : activeTypeTab.value,
createTimeStart: createTimeRange.value?.[0] || undefined,
createTimeEnd: createTimeRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -945,6 +974,7 @@
function handleReset() {
searchForm.keyword = ''
searchForm.workOrderStatus = ''
createTimeRange.value = getDefaultLast30DaysRange()
pagination.page = 1
loadData()
}
@@ -1015,7 +1045,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, createTimeRange],
() => {
pagination.page = 1
loadData()
@@ -46,13 +46,35 @@
<el-option label="科研捐赠" value="research_donation" />
</el-select>
</el-form-item>
<el-form-item v-if="activeTab === 'pending'" label="预计到达时间">
<el-date-picker
v-model="etaDateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item v-else label="接收时间">
<el-date-picker
v-model="receptionTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">
{{
activeTab === 'pending'
? '勾选多个样本后可批量接收(同快递可一次登记,每项单独填写)。'
: '双击行可查看接收详情;也可点击「编辑」修改接收登记。'
? '勾选多个样本后可批量接收;预计到达时间默认近 30 天。'
: '双击行可查看接收详情;接收时间默认近 30 天。'
}}
</span>
<div class="filter-actions-end">
@@ -607,6 +629,23 @@
const activeTab = ref('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)]
}
const etaDateRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
const receptionTimeRange = ref<[string, string] | null>(getDefaultLast30DaysRange())
const tableData = ref<Record<string, unknown>[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
@@ -1027,16 +1066,24 @@
async function loadData() {
loading.value = true
try {
const params = {
const baseParams = {
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
sourceType: searchForm.sourceType || undefined,
sourceType: searchForm.sourceType || undefined
}
const res =
activeTab.value === 'pending'
? await fetchSampleReceptionPendingList(params)
: await fetchSampleReceptionReceivedList(params)
? await fetchSampleReceptionPendingList({
...baseParams,
etaTimeStart: etaDateRange.value?.[0] || undefined,
etaTimeEnd: etaDateRange.value?.[1] || undefined
})
: await fetchSampleReceptionReceivedList({
...baseParams,
receptionTimeStart: receptionTimeRange.value?.[0] || undefined,
receptionTimeEnd: receptionTimeRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
@@ -1060,6 +1107,11 @@
function handleReset() {
searchForm.keyword = ''
searchForm.sourceType = ''
if (activeTab.value === 'pending') {
etaDateRange.value = getDefaultLast30DaysRange()
} else {
receptionTimeRange.value = getDefaultLast30DaysRange()
}
pagination.page = 1
loadData()
}
@@ -1196,7 +1248,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, etaDateRange, receptionTimeRange],
() => {
pagination.page = 1
loadData()
@@ -1,10 +1,10 @@
<!-- 捐赠协议审核 -->
<!-- 捐赠协议管理 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<header class="page-head">
<h1>捐赠协议审核</h1>
<p class="page-desc">审核与管理科研捐赠协议数据。</p>
<h1>捐赠协议管理</h1>
<p class="page-desc">管理科研捐赠协议数据,含审核与状态变更。</p>
</header>
<section class="panel">
@@ -32,9 +32,32 @@
<el-option label="转细胞存储" value="zckc" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="样本采集时间">
<el-date-picker
v-model="sampleCollectTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">列表为概要信息,详情见弹窗。</span>
<span class="filter-hint">列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -343,11 +366,27 @@
defineOptions({ name: 'ResearchDonationAudit' })
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 sampleCollectTimeRange = ref<[string, string] | null>(null)
const tableData = ref<ResearchDonationRow[]>([])
const selectedRows = ref<ResearchDonationRow[]>([])
const loading = ref(false)
@@ -364,7 +403,11 @@
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
donationStatus: searchForm.donationStatus || undefined
donationStatus: searchForm.donationStatus || undefined,
createTimeStart: createTimeRange.value?.[0] || undefined,
createTimeEnd: createTimeRange.value?.[1] || undefined,
sampleCollectTimeStart: sampleCollectTimeRange.value?.[0] || undefined,
sampleCollectTimeEnd: sampleCollectTimeRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -381,6 +424,8 @@
function handleReset() {
searchForm.keyword = ''
searchForm.donationStatus = ''
createTimeRange.value = getDefaultCreateTimeRange()
sampleCollectTimeRange.value = null
pagination.page = 1
loadData()
}
@@ -496,7 +541,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, createTimeRange, sampleCollectTimeRange],
() => {
pagination.page = 1
loadData()
@@ -32,9 +32,32 @@
<el-option label="转细胞存储" value="zckc" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="样本采集时间">
<el-date-picker
v-model="sampleCollectTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">列表为概要信息,详情见弹窗。</span>
<span class="filter-hint">列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -280,11 +303,27 @@
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 sampleCollectTimeRange = ref<[string, string] | null>(null)
const tableData = ref<ResearchDonationRow[]>([])
const selectedRows = ref<ResearchDonationRow[]>([])
const loading = ref(false)
@@ -301,7 +340,11 @@
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
donationStatus: searchForm.donationStatus || undefined
donationStatus: searchForm.donationStatus || undefined,
createTimeStart: createTimeRange.value?.[0] || undefined,
createTimeEnd: createTimeRange.value?.[1] || undefined,
sampleCollectTimeStart: sampleCollectTimeRange.value?.[0] || undefined,
sampleCollectTimeEnd: sampleCollectTimeRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -318,6 +361,8 @@
function handleReset() {
searchForm.keyword = ''
searchForm.donationStatus = ''
createTimeRange.value = getDefaultCreateTimeRange()
sampleCollectTimeRange.value = null
pagination.page = 1
loadData()
}
@@ -387,7 +432,7 @@
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
[searchForm, createTimeRange, sampleCollectTimeRange],
() => {
pagination.page = 1
loadData()