This commit is contained in:
zlwu committed 2026-10-13 15:18:01 +08:00
1 parent d08f03ab03
commit cc843535f5
154 files changed
+17837 -5153

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@ export type QcProtocolPaymentSummary = {
overallPayStatusTagType?: string
installmentCount?: number | null
paymentDetails?: QcProtocolPaymentDetailSummary[]
/** 第 1 期多笔转账中最后一笔的转账时间(协议列表「首期缴费时间」优先用) */
/** 第 1 期多笔转账中最早一笔的转账时间(协议列表「首期缴费时间」优先用) */
firstPayTransferTime?: string
}
@@ -354,8 +354,8 @@
useCommon().refresh()
return
}
// 强制重新加载:先将目标组件从 KeepAlive 缓存中排除
store.addKeepAliveExclude(item)
// 强制重新加载:先将目标组件从 KeepAlive 缓存中排除(force:标签仍在 opened 中也要排除)
store.addKeepAliveExclude(item, { force: true })
// 等待 Vue 处理 exclude 变更、清除缓存后再导航
nextTick(() => {
router.push({
+16 -6
View File
@@ -46,6 +46,7 @@ import { useCommon } from '@/hooks/core/useCommon'
import { useMenuStore } from '@/store/modules/menu'
import { useSettingStore } from '@/store/modules/setting'
import { getFirstSecondLevelMenuPath, isWorkbenchRoute } from '@/utils/navigation/route'
import { shouldAddKeepAliveExclude } from '@/utils/navigation/keep-alive-exclude'
interface WorktabState {
current: Partial<WorkTab>
@@ -428,14 +429,23 @@ export const useWorktabStore = defineStore(
/**
* 将指定选项卡添加到 keepAlive 排除列表中
* @param options.force 点击已打开标签切换时传 true,即使标签仍在 opened 中也排除缓存以强制重新挂载
*/
const addKeepAliveExclude = (tab: WorkTab): void => {
if (!tab.keepAlive || !tab.name) return
// 同 name 多开:仍有其它同名标签时勿排除,否则会清掉其它实例缓存
if (opened.value.some((t) => t.name === tab.name)) return
const addKeepAliveExclude = (tab: WorkTab, options?: { force?: boolean }): void => {
const name = tab.name
if (
!shouldAddKeepAliveExclude({
tabName: name,
keepAlive: tab.keepAlive,
openedHasSameName: opened.value.some((t) => t.name === name),
force: options?.force
})
) {
return
}
if (!keepAliveExclude.value.includes(tab.name)) {
keepAliveExclude.value.push(tab.name)
if (name && !keepAliveExclude.value.includes(name)) {
keepAliveExclude.value.push(name)
}
}
+1
View File
@@ -8,3 +8,4 @@
export * from './jump'
export * from './worktab'
export * from './route'
export * from './keep-alive-exclude'
@@ -0,0 +1,58 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { shouldAddKeepAliveExclude } from './keep-alive-exclude'
describe('shouldAddKeepAliveExclude', () => {
it('returns false when tab has no name or keepAlive is off', () => {
assert.equal(
shouldAddKeepAliveExclude({
tabName: undefined,
keepAlive: true,
openedHasSameName: false
}),
false
)
assert.equal(
shouldAddKeepAliveExclude({
tabName: 'CellStorageProtocolAudit',
keepAlive: false,
openedHasSameName: false
}),
false
)
})
it('skips exclude when same-name tab still open (close / worktabMulti guard)', () => {
assert.equal(
shouldAddKeepAliveExclude({
tabName: 'CellStorageProtocolAudit',
keepAlive: true,
openedHasSameName: true
}),
false
)
})
it('allows exclude when no same-name tab remains open', () => {
assert.equal(
shouldAddKeepAliveExclude({
tabName: 'ProductionPreTransitSample',
keepAlive: true,
openedHasSameName: false
}),
true
)
})
it('force=true excludes even when the tab is still in opened (tab click reload)', () => {
assert.equal(
shouldAddKeepAliveExclude({
tabName: 'CellStorageProtocolAudit',
keepAlive: true,
openedHasSameName: true,
force: true
}),
true
)
})
})
@@ -0,0 +1,17 @@
/**
* 判断是否应将组件名加入 KeepAlive exclude,以清除缓存并强制重新挂载。
*
* - 关闭标签:若仍有同名标签(worktabMulti),勿排除,避免清掉其它实例缓存
* - 点击已打开标签切换:force=true,即使标签仍在 opened 中也要排除,以强制重新加载
*/
export function shouldAddKeepAliveExclude(params: {
tabName?: string
keepAlive?: boolean
openedHasSameName: boolean
force?: boolean
}): boolean {
if (!params.keepAlive || !params.tabName) return false
if (params.force) return true
if (params.openedHasSameName) return false
return true
}
@@ -1,4 +1,4 @@
<!-- 客户管理 -->
<!-- 客户基础信息 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
@@ -27,9 +27,6 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item label="协议编号">
<el-input v-model="form.protocolNo" placeholder="可选,转正式时可预占" clearable />
</el-form-item>
<el-form-item label="母亲姓名" required>
<el-input v-model="form.motherName" placeholder="请输入母亲姓名" clearable />
</el-form-item>
@@ -828,7 +825,6 @@
try {
const payload: Record<string, unknown> = {
orderDate: form.orderDate || undefined,
protocolNo: form.protocolNo || undefined,
motherName: form.motherName,
expectedDeliveryDate: form.expectedDeliveryDate || undefined,
regionId: form.regionId || undefined,
@@ -174,15 +174,15 @@
<template #default="{ row }">{{ row.motherName || '—' }}</template>
</el-table-column>
<el-table-column
prop="protocolNo"
label="协议编号"
prop="sampleCode"
label="样本编码"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<span class="mono">{{ row.protocolNo || '—' }}</span>
<span class="mono">{{ row.sampleCode || '—' }}</span>
</template>
</el-table-column>
<el-table-column
@@ -325,10 +325,10 @@
/>
</el-select>
</el-form-item>
<el-form-item label="母亲家庭住址" class="form-col-span-2">
<el-input v-model="formData.motherAddress" placeholder="请输入" />
</el-form-item>
</div>
<el-form-item label="母亲家庭住址">
<el-input v-model="formData.motherAddress" placeholder="请输入" />
</el-form-item>
</div>
<!-- 父亲信息 -->
@@ -2521,6 +2521,16 @@
s.sampleStatus != null && String(s.sampleStatus).trim() !== ''
? String(s.sampleStatus).trim()
: null,
transport: {
transportType: transport?.transportType || '',
expressCompanyId: transport?.expressCompanyId || '',
waybillNo: transport?.waybillNo || '',
destSiteId: transport?.destSiteId || '',
expressFee: transport?.expressFee ?? null,
etaTime: transport?.etaTime || '',
carrierName: transport?.carrierName || '',
carrierPhone: transport?.carrierPhone || ''
},
freightId: freight?.id || s.freightId || undefined,
freightDraft: freightDraft || undefined
}))
@@ -531,10 +531,22 @@
}
.section-title {
display: flex;
align-items: center;
margin: 12px 0 8px;
font-size: 13px;
font-weight: 600;
color: var(--mint-text, #0f172a);
&::before {
content: '';
width: 3px;
height: 14px;
margin-right: 8px;
flex-shrink: 0;
border-radius: 2px;
background: var(--mint-primary, #047857);
}
}
.action-btn--danger {
@@ -1,22 +1,5 @@
<template>
<div class="unified-sample-info-page">
<div v-if="visibleTabs.length > 1" class="panel-pill-tabs panel-pill-tabs--lg">
<div class="panel-pill-tabs__track" role="tablist" aria-label="来源">
<button
v-for="item in visibleTabs"
:key="item.key"
type="button"
role="tab"
class="panel-pill-tabs__btn"
:class="{ 'is-active': activeSource === item.key }"
:aria-selected="activeSource === item.key"
@click="switchSource(item.key)"
>
{{ item.title }}
</button>
</div>
</div>
<div class="filter-block">
<el-form
:model="searchForm"
@@ -497,7 +480,6 @@
import DonationViewDialog from '@/views/research-donation/list/DonationViewDialog.vue'
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
import SampleInfoQuickRegisterDialog from './SampleInfoQuickRegisterDialog.vue'
import { useMenuPerm } from '@/hooks/core/useMenuPerm'
import {
donationStatusDisplayLabel,
donationStatusTagType,
@@ -521,23 +503,8 @@
defaultSource: 'cell_storage' | 'research_donation'
}>()
const { hasMenuPerm } = useMenuPerm()
const route = useRoute()
const router = useRouter()
type SourceKey = 'cell_storage' | 'research_donation'
const allTabs: Array<{ key: SourceKey; title: string; perm: string }> = [
{ key: 'cell_storage', title: '细胞存储', perm: 'cell-storage:sample-info:list' },
{
key: 'research_donation',
title: '科研捐赠',
perm: 'research-donation:sample-info:list'
}
]
const visibleTabs = computed(() => allTabs.filter((t) => hasMenuPerm(t.perm)))
const activeSource = ref<SourceKey>(props.defaultSource)
/** 客户服务管理仅细胞存储;科研捐赠隐藏路由仍可通过 defaultSource 进入 */
const activeSource = computed(() => props.defaultSource)
const isCellStorage = computed(() => activeSource.value === 'cell_storage')
const searchForm = reactive({
@@ -600,16 +567,6 @@
return donationStatusTagType(String(row.donationStatus || ''))
}
function switchSource(key: SourceKey) {
activeSource.value = key
protocolViewVisible.value = false
donationViewVisible.value = false
selectedRows.value = []
router.replace({ query: { ...route.query, tab: key } })
pagination.page = 1
loadData()
}
async function loadData() {
loading.value = true
try {
@@ -731,15 +688,6 @@
)
onMounted(() => {
const tab = String(route.query.tab || '')
const allowed = visibleTabs.value.map((t) => t.key)
if (allowed.includes(tab as SourceKey)) {
activeSource.value = tab as SourceKey
} else if (allowed.includes(props.defaultSource)) {
activeSource.value = props.defaultSource
} else if (allowed.length) {
activeSource.value = allowed[0]
}
loadData()
})
</script>
@@ -34,7 +34,7 @@
{ label: '用户管理', path: '/system/user' },
{ label: '商品分类', path: '/system/category' },
{ label: '租户管理', path: '/system/tenant' },
{ label: '客户管理', path: '/basic-info/customer' }
{ label: '客户基础信息', path: '/basic-info/customer' }
]
const go = (path: string) => {
@@ -1451,6 +1451,16 @@
s.sampleStatus != null && String(s.sampleStatus).trim() !== ''
? String(s.sampleStatus).trim()
: null,
transport: {
transportType: transport?.transportType || '',
expressCompanyId: transport?.expressCompanyId || '',
waybillNo: transport?.waybillNo || '',
destSiteId: transport?.destSiteId || '',
expressFee: transport?.expressFee ?? null,
etaTime: transport?.etaTime || '',
carrierName: transport?.carrierName || '',
carrierPhone: transport?.carrierPhone || ''
},
freightId: freight?.id || s.freightId || undefined,
freightDraft: freightDraft || undefined
}))
@@ -247,10 +247,14 @@
</div>
</div>
<div
v-if="row.freight?.id || row.freight?.transportMode"
v-if="
row.freight?.id ||
row.freight?.transportMode ||
row.transport?.etaTime
"
class="sample-card__section sample-card__section--transport"
>
<div class="sample-card__section-label">运费信息</div>
<div class="sample-card__section-label">运输与运费</div>
<div class="data-grid data-grid--4">
<div class="data-item">
<div class="data-label">运输方式</div>
@@ -270,6 +274,10 @@
}}</div
>
</div>
<div class="data-item">
<div class="data-label">预计到达</div>
<div class="data-value">{{ dash(row.transport?.etaTime) }}</div>
</div>
</div>
</div>
<div class="sample-card__section sample-card__section--reception">