fix: 协议子资源单事务保存,并补齐样本编码/OA/质检等近期能力
锁定缴费校验失败时整单回滚,避免样本半成功落库;前端禁止删除锁定缴费样本,改码走更正编码。同步落地样本更正、采集微信通知、OA提及未读、细胞产品套餐与相关迁移文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
19bc66ba45
commit
5dad1bb0e3
142 files changed
+95584
-65717
No files matched your search
@@ -213,6 +213,7 @@
|
||||
:unit-options="unitOptions"
|
||||
:unit-options-loading="unitLoading"
|
||||
:show-price="true"
|
||||
:sample-payments="formData.samplePayments"
|
||||
@price-change="onSamplePriceChange"
|
||||
@search-sample-code="remoteSearchSampleCodes"
|
||||
@search-sample-type="remoteSearchSampleTypes"
|
||||
|
||||
@@ -372,6 +372,7 @@
|
||||
parsePreferentialPrice,
|
||||
resolvePerSamplePaymentDefaults,
|
||||
samplePaymentHasConfirmedTransfer,
|
||||
samplePaymentHasLockedTransfer,
|
||||
schedulePaidArrears,
|
||||
shouldAutoFillLastPeriodDue,
|
||||
shouldReplaceContractWithPreferentialPrice
|
||||
@@ -569,19 +570,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
function paymentHasLockedTransfer(pay: Record<string, any>) {
|
||||
return (pay.schedules || []).some((s: Record<string, any>) =>
|
||||
(s.transfers || []).some((t: Record<string, any>) => t.payStatus === 'yjf')
|
||||
)
|
||||
}
|
||||
|
||||
/** 优惠价变更时同步合同;打开编辑不覆盖库内金额(仅合同为空时补齐) */
|
||||
function applyPreferentialPriceToPayment(
|
||||
pay: Record<string, any>,
|
||||
sample: Record<string, any>,
|
||||
force = false
|
||||
): Record<string, any> {
|
||||
if (paymentHasLockedTransfer(pay)) return pay
|
||||
if (samplePaymentHasLockedTransfer(pay)) return pay
|
||||
const price = parsePreferentialPrice(sample)
|
||||
if (price == null) return pay
|
||||
if (!force && !shouldReplaceContractWithPreferentialPrice(pay.contractAmount, price)) {
|
||||
@@ -1114,6 +1109,26 @@
|
||||
next.push(emptyDjPayment(sample))
|
||||
}
|
||||
}
|
||||
// 锁定缴费不可因样本删改/换码从提交列表中消失(对齐后端「不能省略或删除」)
|
||||
const keptCodes = new Set(next.map((p) => String(p.sampleCode || '')))
|
||||
const keptIds = new Set(
|
||||
next.map((p) => String(p.id || p.samplePaymentId || '')).filter(Boolean)
|
||||
)
|
||||
let restoredLocked = false
|
||||
for (const pay of existing) {
|
||||
if (!samplePaymentHasLockedTransfer(pay)) continue
|
||||
const id = String(pay.id || pay.samplePaymentId || '')
|
||||
const code = String(pay.sampleCode || '')
|
||||
if ((id && keptIds.has(id)) || (code && keptCodes.has(code))) continue
|
||||
next.push(pay)
|
||||
restoredLocked = true
|
||||
}
|
||||
if (restoredLocked) {
|
||||
ElMessage.warning(
|
||||
'存在锁定缴费方案的样本不可删除或改码,请使用客户服务「更正编码」'
|
||||
)
|
||||
}
|
||||
|
||||
lastPreferentialByCode.value = nextPriceMap
|
||||
const same =
|
||||
next.length === existing.length &&
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="更正样本编码"
|
||||
width="640px"
|
||||
class="dialog-form-layout"
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="onClosed"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="form-grid-2">
|
||||
<el-form-item label="当前样本编码" class="form-col-span-2">
|
||||
<el-input :model-value="oldSampleCode" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="新样本编码" prop="newSampleCode" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="form.newSampleCode"
|
||||
placeholder="请输入正确的样本编码"
|
||||
clearable
|
||||
maxlength="32"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="更正原因" prop="reason" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="form.reason"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="例如:客户填错编码"
|
||||
maxlength="512"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div v-if="preview" class="preview-block">
|
||||
<div v-if="!preview.ok" class="preview-blockers">
|
||||
<div v-for="(b, i) in preview.blockers" :key="i" class="blocker-line">{{ b }}</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="preview-summary">
|
||||
预计影响 <strong>{{ preview.totalAffectedRows }}</strong> 行(含源表)
|
||||
</div>
|
||||
<el-table :data="preview.impacts" size="small" stripe class="preview-table" max-height="220">
|
||||
<el-table-column prop="label" label="业务表" min-width="160" />
|
||||
<el-table-column prop="table" label="表名" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="count" label="行数" min-width="72" align="center" />
|
||||
</el-table>
|
||||
<ul class="preview-hints">
|
||||
<li>实验室入库编码不会修改</li>
|
||||
<li>细胞产品码不会自动修改(同名产品码:旧 {{ hintOld }} / 新 {{ hintNew }})</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button :loading="previewing" @click="handlePreview">
|
||||
<el-icon><View /></el-icon>
|
||||
预览影响
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="applying"
|
||||
:disabled="!preview?.ok"
|
||||
@click="handleApply"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
确认更正
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, View } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import {
|
||||
fetchApplySampleCodeCorrection,
|
||||
fetchPreviewSampleCodeCorrection,
|
||||
type SampleCodeCorrectionPreview,
|
||||
type SampleCodeCorrectionSource
|
||||
} from '@/api/sample-code-correction'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
receptionSource: SampleCodeCorrectionSource
|
||||
oldSampleCode: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const form = reactive({
|
||||
newSampleCode: '',
|
||||
reason: ''
|
||||
})
|
||||
const rules: FormRules = {
|
||||
newSampleCode: [{ required: true, message: '请输入新样本编码', trigger: 'blur' }],
|
||||
reason: [{ required: true, message: '请填写更正原因', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const preview = ref<SampleCodeCorrectionPreview | null>(null)
|
||||
const previewing = ref(false)
|
||||
const applying = ref(false)
|
||||
|
||||
const hintOld = computed(() => preview.value?.hints?.sameProductCodeAsOld ?? 0)
|
||||
const hintNew = computed(() => preview.value?.hints?.sameProductCodeAsNew ?? 0)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(open) => {
|
||||
if (open) {
|
||||
form.newSampleCode = ''
|
||||
form.reason = ''
|
||||
preview.value = null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
function onClosed() {
|
||||
preview.value = null
|
||||
}
|
||||
|
||||
async function handlePreview() {
|
||||
const ok = await formRef.value?.validate().catch(() => false)
|
||||
if (!ok) return
|
||||
previewing.value = true
|
||||
try {
|
||||
const data = await fetchPreviewSampleCodeCorrection({
|
||||
receptionSource: props.receptionSource,
|
||||
oldSampleCode: props.oldSampleCode,
|
||||
newSampleCode: form.newSampleCode.trim(),
|
||||
reason: form.reason.trim()
|
||||
})
|
||||
preview.value = data
|
||||
if (!data.ok) {
|
||||
ElMessage.warning(data.blockers?.[0] || '无法更正')
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '预览失败')
|
||||
} finally {
|
||||
previewing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleApply() {
|
||||
if (!preview.value?.ok) {
|
||||
ElMessage.warning('请先预览并通过校验')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认将样本编码「${props.oldSampleCode}」更正为「${form.newSampleCode.trim()}」?将级联更新 ${preview.value.totalAffectedRows} 行相关数据。`,
|
||||
'确认更正',
|
||||
{ type: 'warning', confirmButtonText: '确认更正', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
applying.value = true
|
||||
try {
|
||||
await fetchApplySampleCodeCorrection({
|
||||
receptionSource: props.receptionSource,
|
||||
oldSampleCode: props.oldSampleCode,
|
||||
newSampleCode: form.newSampleCode.trim(),
|
||||
reason: form.reason.trim()
|
||||
})
|
||||
ElMessage.success('样本编码已更正')
|
||||
visible.value = false
|
||||
emit('success')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || '更正失败')
|
||||
} finally {
|
||||
applying.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.preview-block {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.preview-blockers {
|
||||
color: var(--el-color-danger);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.preview-summary {
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text, #334155);
|
||||
}
|
||||
|
||||
.preview-hints {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 18px;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -393,17 +393,24 @@
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" header-align="center">
|
||||
<el-table-column label="操作" min-width="360" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!row.sampleCode"
|
||||
@click="openCodeCorrection(row)"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>更正编码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="isCellStorage && supportsStorageCertQc(row.sampleTypeName)"
|
||||
class="btn-op-item"
|
||||
@click="openStorageCertQcView(row)"
|
||||
@click="openStorageCertQcEdit(row)"
|
||||
>
|
||||
<el-icon><Document /></el-icon>查看储存证的质检信息
|
||||
<el-icon><Document /></el-icon>填写储存证质检信息
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -447,10 +454,16 @@
|
||||
/>
|
||||
<StorageCertQcDialog
|
||||
v-model:visible="storageCertQcVisible"
|
||||
mode="view"
|
||||
mode="edit"
|
||||
:sample-code="storageCertQcSampleCode"
|
||||
:sample-type-name="storageCertQcSampleTypeName"
|
||||
/>
|
||||
<SampleCodeCorrectionDialog
|
||||
v-model="codeCorrectionVisible"
|
||||
:reception-source="activeSource"
|
||||
:old-sample-code="codeCorrectionOldCode"
|
||||
@success="loadData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -460,6 +473,7 @@
|
||||
ChatDotRound,
|
||||
Collection,
|
||||
Document,
|
||||
EditPen,
|
||||
Files,
|
||||
Location,
|
||||
Phone,
|
||||
@@ -480,6 +494,7 @@
|
||||
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 SampleCodeCorrectionDialog from './SampleCodeCorrectionDialog.vue'
|
||||
import {
|
||||
donationStatusDisplayLabel,
|
||||
donationStatusTagType,
|
||||
@@ -535,6 +550,8 @@
|
||||
const storageCertQcVisible = ref(false)
|
||||
const storageCertQcSampleCode = ref('')
|
||||
const storageCertQcSampleTypeName = ref('')
|
||||
const codeCorrectionVisible = ref(false)
|
||||
const codeCorrectionOldCode = ref('')
|
||||
|
||||
function sexLabel(sex: unknown) {
|
||||
const s = String(sex || '').trim()
|
||||
@@ -543,16 +560,26 @@
|
||||
return s || '—'
|
||||
}
|
||||
|
||||
function openStorageCertQcView(row: Record<string, unknown>) {
|
||||
function openStorageCertQcEdit(row: Record<string, unknown>) {
|
||||
storageCertQcSampleCode.value = String(row.sampleCode || '')
|
||||
storageCertQcSampleTypeName.value = String(row.sampleTypeName || '')
|
||||
if (!storageCertQcSampleCode.value) {
|
||||
ElMessage.warning('缺少样本编码,无法查看')
|
||||
ElMessage.warning('缺少样本编码,无法填写')
|
||||
return
|
||||
}
|
||||
storageCertQcVisible.value = true
|
||||
}
|
||||
|
||||
function openCodeCorrection(row: Record<string, unknown>) {
|
||||
const code = String(row.sampleCode || '').trim()
|
||||
if (!code) {
|
||||
ElMessage.warning('缺少样本编码,无法更正')
|
||||
return
|
||||
}
|
||||
codeCorrectionOldCode.value = code
|
||||
codeCorrectionVisible.value = true
|
||||
}
|
||||
|
||||
function statusLabel(row: Record<string, unknown>) {
|
||||
if (isCellStorage.value) {
|
||||
return protocolStatusDisplayLabel(String(row.protocolStatus || ''), String(row.voidType || ''))
|
||||
|
||||
@@ -74,7 +74,12 @@
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.appName || row.title || '-' }}</template>
|
||||
<template #default="{ row }">
|
||||
<span class="mention-flow-name">
|
||||
<span v-if="!row.isRead" class="mention-unread-dot" aria-hidden="true" />
|
||||
{{ row.appName || row.title || '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="initiatorUserName"
|
||||
@@ -116,6 +121,13 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="阅读状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="row.isRead ? 'info' : 'danger'">
|
||||
{{ row.isRead ? '已读' : '未读' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="openDetail(row)">
|
||||
@@ -156,9 +168,14 @@
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Printer, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
||||
import { fetchOaInstanceDetail, fetchOaTasks } from '@/api/oaApproval'
|
||||
import {
|
||||
fetchMarkOaMentionRead,
|
||||
fetchOaInstanceDetail,
|
||||
fetchOaTasks
|
||||
} from '@/api/oaApproval'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { exportOaApprovalPdf } from '@/utils/oa-approval-print'
|
||||
import { refreshOaMenuBadges } from '@/utils/oa-menu-badge'
|
||||
|
||||
defineOptions({ name: 'OaApprovalMention' })
|
||||
|
||||
@@ -211,9 +228,18 @@
|
||||
return map[s] || s
|
||||
}
|
||||
|
||||
function openDetail(row: any) {
|
||||
const id = row.id
|
||||
async function openDetail(row: any) {
|
||||
const id = String(row?.id || '').trim()
|
||||
if (!id) return
|
||||
if (!row.isRead) {
|
||||
try {
|
||||
await fetchMarkOaMentionRead(id)
|
||||
row.isRead = true
|
||||
} catch {
|
||||
/* 仍允许进入详情 */
|
||||
}
|
||||
void refreshOaMenuBadges()
|
||||
}
|
||||
router.push({ path: `/system-manage/oa-approval/instances/${id}` })
|
||||
}
|
||||
|
||||
@@ -289,6 +315,29 @@
|
||||
{ debounce: 300, deep: true }
|
||||
)
|
||||
|
||||
onMounted(load)
|
||||
onActivated(load)
|
||||
onMounted(() => {
|
||||
void load()
|
||||
void refreshOaMenuBadges()
|
||||
})
|
||||
onActivated(() => {
|
||||
void load()
|
||||
void refreshOaMenuBadges()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mention-flow-name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mention-unread-dot {
|
||||
flex-shrink: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--el-color-danger);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,405 @@
|
||||
<!-- 细胞产品基础信息维护 · 包装 Tab -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="cell-product-package-tab">
|
||||
<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.keyword" placeholder="包装编码/名称" clearable />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<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">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增包装
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<el-icon><Delete /></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"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="packageCode"
|
||||
label="包装编码"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="packageName"
|
||||
label="包装名称"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="120"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-dropdown
|
||||
trigger="hover"
|
||||
@command="(cmd: string) => handleDropdownCommand(cmd, row)"
|
||||
>
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="package-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="包装编码" prop="packageCode">
|
||||
<el-input v-model="formData.packageCode" placeholder="请输入包装编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="包装名称" prop="packageName">
|
||||
<el-input v-model="formData.packageName" placeholder="请输入包装名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="formData.remark" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewRow?.packageName || '包装详情'"
|
||||
:icon="Box"
|
||||
:code="viewRow?.packageCode"
|
||||
code-label="包装编码"
|
||||
:sections="viewSections"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatDisplayOrDash } from '@/utils/datetime-display'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Close,
|
||||
Check,
|
||||
View,
|
||||
Edit,
|
||||
Delete,
|
||||
Box,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
More
|
||||
} from '@element-plus/icons-vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import {
|
||||
fetchCellProductPackageList,
|
||||
fetchCreateCellProductPackage,
|
||||
fetchUpdateCellProductPackage,
|
||||
fetchDeleteCellProductPackage
|
||||
} from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'CellProductPackageTab' })
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.CellProductPackage.CellProductPackageItem[]>([])
|
||||
const selectedRows = ref<Api.CellProductPackage.CellProductPackageItem[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增包装')
|
||||
const isEdit = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const editingId = ref<string | null>(null)
|
||||
const viewVisible = ref(false)
|
||||
const viewRow = ref<Api.CellProductPackage.CellProductPackageItem | null>(null)
|
||||
|
||||
const formData = reactive({
|
||||
packageCode: '',
|
||||
packageName: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
packageCode: [{ required: true, message: '请输入包装编码', trigger: 'blur' }],
|
||||
packageName: [{ required: true, message: '请输入包装名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: Api.CellProductPackage.CellProductPackageItem[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
const viewSections = computed(() => {
|
||||
if (!viewRow.value) return []
|
||||
const row = viewRow.value
|
||||
return [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '包装编码', value: row.packageCode },
|
||||
{ label: '包装名称', value: row.packageName },
|
||||
{ label: '备注', value: row.remark, span: true }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchCellProductPackageList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.packageCode = ''
|
||||
formData.packageName = ''
|
||||
formData.remark = ''
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增包装'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
function handleEdit(row: Api.CellProductPackage.CellProductPackageItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑包装'
|
||||
editingId.value = row.id
|
||||
formData.packageCode = row.packageCode || ''
|
||||
formData.packageName = row.packageName || ''
|
||||
formData.remark = row.remark || ''
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate?.()
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
packageCode: formData.packageCode.trim(),
|
||||
packageName: formData.packageName.trim(),
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateCellProductPackage(editingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateCellProductPackage(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleView(row: Api.CellProductPackage.CellProductPackageItem) {
|
||||
viewRow.value = row
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function handleDropdownCommand(cmd: string, row: Api.CellProductPackage.CellProductPackageItem) {
|
||||
if (cmd === 'edit') handleEdit(row)
|
||||
else if (cmd === 'delete') handleDelete(row)
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.CellProductPackage.CellProductPackageItem) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该包装吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteCellProductPackage(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个包装吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await withFullscreenLoading(async () => {
|
||||
for (const id of ids) {
|
||||
await fetchDeleteCellProductPackage(id)
|
||||
}
|
||||
ElMessage.success('批量删除成功')
|
||||
selectedRows.value = []
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.batchDelete)
|
||||
}
|
||||
// 输入检索条件时自动搜索(防抖 300ms)
|
||||
watchDebounced(
|
||||
searchForm,
|
||||
() => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
},
|
||||
{ debounce: 300, deep: true }
|
||||
)
|
||||
|
||||
defineExpose({ handleAdd, loadData })
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.package-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,214 +1,165 @@
|
||||
<!-- 细胞产品基础信息维护 · 价格体系 Tab -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div :class="layout === 'kanban' ? 'kanban-col-body' : 'cell-product-price-tab'">
|
||||
<template v-if="layout === 'kanban'">
|
||||
<div class="kanban-col-search">
|
||||
<el-input v-model="searchForm.keyword" placeholder="项目名称/编码" clearable />
|
||||
</div>
|
||||
<div class="kanban-list" v-loading="loading">
|
||||
<article
|
||||
v-for="row in tableData"
|
||||
:key="row.id"
|
||||
class="entity-card"
|
||||
@dblclick="handleView(row)"
|
||||
<div class="cell-product-price-tab">
|
||||
<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.keyword" placeholder="项目名称/编码" clearable />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<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">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增价格
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<div class="entity-card-del" @click.stop>
|
||||
<el-button class="btn-op-item" title="删除" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="entity-card-body">
|
||||
<h3 class="entity-card-title">{{ row.projectName }}</h3>
|
||||
<div class="entity-card-meta">{{ row.projectCode }}</div>
|
||||
<div v-if="row.cellTypeName || row.specText" class="entity-card-tags">
|
||||
<span v-if="row.cellTypeName" class="tag success">{{ row.cellTypeName }}</span>
|
||||
<span v-if="row.specText" class="tag muted">{{ row.specText }}</span>
|
||||
</div>
|
||||
<div class="entity-card-ft" @click.stop>
|
||||
<span
|
||||
class="entity-card-ft-val"
|
||||
:class="{ 'entity-card-ft-val--muted': row.unitPrice == null }"
|
||||
>
|
||||
{{ row.unitPrice != null ? `${formatMoneyDisplay(row.unitPrice)} 元` : '未定价' }}
|
||||
</span>
|
||||
<div class="entity-card-ft-actions">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div v-if="!loading && !tableData.length" class="kanban-empty">
|
||||
<span>暂无价格数据</span>
|
||||
<span class="kanban-empty__hint">点击列头「新增」添加产品价格</span>
|
||||
</div>
|
||||
<el-icon><Delete /></el-icon>
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<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.keyword" placeholder="项目名称/编码" clearable />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<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">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增价格
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<el-icon><Delete /></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"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="projectName"
|
||||
label="项目名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="projectCode"
|
||||
label="项目编码"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="cellTypeName"
|
||||
label="细胞类型名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="specText"
|
||||
label="规格"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="unitPrice"
|
||||
label="单价"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="projectName"
|
||||
label="项目名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="projectCode"
|
||||
label="项目编码"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="cellTypeName"
|
||||
label="细胞类型名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="specText"
|
||||
label="规格"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="unitPrice"
|
||||
label="单价"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.unitPrice != null ? `${formatMoneyDisplay(row.unitPrice)} 元` : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center">
|
||||
<template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="160"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
<template #default="{ row }">
|
||||
{{ row.unitPrice != null ? `${formatMoneyDisplay(row.unitPrice)} 元` : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="160"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-dropdown trigger="hover" @command="(cmd) => handleDropdownCommand(cmd, row)">
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<el-dropdown trigger="hover" @command="(cmd) => handleDropdownCommand(cmd, row)">
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</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>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
@@ -343,11 +294,6 @@
|
||||
|
||||
defineOptions({ name: 'CellProductPriceTab' })
|
||||
|
||||
const props = withDefaults(defineProps<{ layout?: 'table' | 'kanban' }>(), { layout: 'table' })
|
||||
const emit = defineEmits<{ (e: 'meta-change', payload: { total: number }): void }>()
|
||||
|
||||
const KANBAN_PAGE_SIZE = 100
|
||||
|
||||
// --- Search ---
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
|
||||
@@ -484,7 +430,6 @@
|
||||
})
|
||||
tableData.value = (res as any)?.list || []
|
||||
pagination.total = (res as any)?.total ?? 0
|
||||
emit('meta-change', { total: pagination.total })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -652,35 +597,11 @@
|
||||
defineExpose({ handleAdd, loadData })
|
||||
|
||||
onMounted(() => {
|
||||
if (props.layout === 'kanban') {
|
||||
pagination.pageSize = KANBAN_PAGE_SIZE
|
||||
}
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.kanban-col-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.kanban-col-search {
|
||||
padding: 8px 10px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kanban-list {
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@import './cell-product-kanban.scss';
|
||||
|
||||
.price-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -1,185 +1,143 @@
|
||||
<!-- 细胞产品基础信息维护 · 规格 Tab -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div :class="layout === 'kanban' ? 'kanban-col-body' : 'cell-product-spec-tab'">
|
||||
<template v-if="layout === 'kanban'">
|
||||
<div class="kanban-col-search">
|
||||
<el-input v-model="searchForm.keyword" placeholder="规格编码/名称" clearable />
|
||||
</div>
|
||||
<div class="kanban-list" v-loading="loading">
|
||||
<article
|
||||
v-for="row in tableData"
|
||||
:key="row.id"
|
||||
class="entity-card"
|
||||
@dblclick="handleView(row)"
|
||||
<div class="cell-product-spec-tab">
|
||||
<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.keyword" placeholder="规格编码/名称" clearable />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<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">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增规格
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<div class="entity-card-del" @click.stop>
|
||||
<el-button class="btn-op-item" title="删除" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="entity-card-body">
|
||||
<h3 class="entity-card-title">{{ row.specName }}</h3>
|
||||
<div class="entity-card-meta">{{ row.specCode }}</div>
|
||||
<div v-if="row.remark" class="entity-card-tags">
|
||||
<span class="tag muted">{{ row.remark }}</span>
|
||||
</div>
|
||||
<div class="entity-card-ft" @click.stop>
|
||||
<div class="entity-card-ft-actions">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div v-if="!loading && !tableData.length" class="kanban-empty">
|
||||
<span>暂无规格数据</span>
|
||||
<span class="kanban-empty__hint">点击列头「新增」添加产品规格</span>
|
||||
</div>
|
||||
<el-icon><Delete /></el-icon>
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<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.keyword" placeholder="规格编码/名称" clearable />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<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">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增规格
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<el-icon><Delete /></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"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="specCode"
|
||||
label="规格编码"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="specName"
|
||||
label="规格名称"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="specCode"
|
||||
label="规格编码"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="specName"
|
||||
label="规格名称"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center">
|
||||
<template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="120"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
<template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="120"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-dropdown
|
||||
trigger="hover"
|
||||
@command="(cmd: string) => handleDropdownCommand(cmd, row)"
|
||||
>
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<el-dropdown
|
||||
trigger="hover"
|
||||
@command="(cmd: string) => handleDropdownCommand(cmd, row)"
|
||||
>
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</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>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
@@ -263,11 +221,6 @@
|
||||
|
||||
defineOptions({ name: 'CellProductSpecTab' })
|
||||
|
||||
const props = withDefaults(defineProps<{ layout?: 'table' | 'kanban' }>(), { layout: 'table' })
|
||||
const emit = defineEmits<{ (e: 'meta-change', payload: { total: number }): void }>()
|
||||
|
||||
const KANBAN_PAGE_SIZE = 100
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.CellProductSpec.CellProductSpecItem[]>([])
|
||||
const selectedRows = ref<Api.CellProductSpec.CellProductSpecItem[]>([])
|
||||
@@ -322,7 +275,6 @@
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
emit('meta-change', { total: pagination.total })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -440,35 +392,11 @@
|
||||
defineExpose({ handleAdd, loadData })
|
||||
|
||||
onMounted(() => {
|
||||
if (props.layout === 'kanban') {
|
||||
pagination.pageSize = KANBAN_PAGE_SIZE
|
||||
}
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.kanban-col-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.kanban-col-search {
|
||||
padding: 8px 10px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kanban-list {
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@import './cell-product-kanban.scss';
|
||||
|
||||
.spec-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
<!-- 细胞产品基础信息维护 · 类型 Tab -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="cell-product-type-tab">
|
||||
<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.keyword" placeholder="类型编码/名称" clearable />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<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">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增类型
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<el-icon><Delete /></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"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="productTypeCode"
|
||||
label="类型编码"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="productTypeName"
|
||||
label="类型名称"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ formatDisplayOrDash(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="120"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-dropdown
|
||||
trigger="hover"
|
||||
@command="(cmd: string) => handleDropdownCommand(cmd, row)"
|
||||
>
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="type-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="类型编码" prop="productTypeCode">
|
||||
<el-input v-model="formData.productTypeCode" placeholder="请输入类型编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型名称" prop="productTypeName">
|
||||
<el-input v-model="formData.productTypeName" placeholder="请输入类型名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="formData.remark" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewRow?.productTypeName || '类型详情'"
|
||||
:icon="Grid"
|
||||
:code="viewRow?.productTypeCode"
|
||||
code-label="类型编码"
|
||||
:sections="viewSections"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatDisplayOrDash } from '@/utils/datetime-display'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Close,
|
||||
Check,
|
||||
View,
|
||||
Edit,
|
||||
Delete,
|
||||
Grid,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
More
|
||||
} from '@element-plus/icons-vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import {
|
||||
fetchCellProductTypeList,
|
||||
fetchCreateCellProductType,
|
||||
fetchUpdateCellProductType,
|
||||
fetchDeleteCellProductType
|
||||
} from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'CellProductTypeTab' })
|
||||
|
||||
const emit = defineEmits<{ (e: 'changed'): void }>()
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.CellProductType.CellProductTypeItem[]>([])
|
||||
const selectedRows = ref<Api.CellProductType.CellProductTypeItem[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增类型')
|
||||
const isEdit = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const editingId = ref<string | null>(null)
|
||||
const viewVisible = ref(false)
|
||||
const viewRow = ref<Api.CellProductType.CellProductTypeItem | null>(null)
|
||||
|
||||
const formData = reactive({
|
||||
productTypeCode: '',
|
||||
productTypeName: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
productTypeCode: [{ required: true, message: '请输入类型编码', trigger: 'blur' }],
|
||||
productTypeName: [{ required: true, message: '请输入类型名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: Api.CellProductType.CellProductTypeItem[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
const viewSections = computed(() => {
|
||||
if (!viewRow.value) return []
|
||||
const row = viewRow.value
|
||||
return [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '类型编码', value: row.productTypeCode },
|
||||
{ label: '类型名称', value: row.productTypeName },
|
||||
{ label: '备注', value: row.remark, span: true }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchCellProductTypeList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.productTypeCode = ''
|
||||
formData.productTypeName = ''
|
||||
formData.remark = ''
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增类型'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
function handleEdit(row: Api.CellProductType.CellProductTypeItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑类型'
|
||||
editingId.value = row.id
|
||||
formData.productTypeCode = row.productTypeCode || ''
|
||||
formData.productTypeName = row.productTypeName || ''
|
||||
formData.remark = row.remark || ''
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate?.()
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
productTypeCode: formData.productTypeCode.trim(),
|
||||
productTypeName: formData.productTypeName.trim(),
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateCellProductType(editingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateCellProductType(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
emit('changed')
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleView(row: Api.CellProductType.CellProductTypeItem) {
|
||||
viewRow.value = row
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function handleDropdownCommand(cmd: string, row: Api.CellProductType.CellProductTypeItem) {
|
||||
if (cmd === 'edit') handleEdit(row)
|
||||
else if (cmd === 'delete') handleDelete(row)
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.CellProductType.CellProductTypeItem) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该类型吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteCellProductType(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
emit('changed')
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个类型吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await withFullscreenLoading(async () => {
|
||||
for (const id of ids) {
|
||||
await fetchDeleteCellProductType(id)
|
||||
}
|
||||
ElMessage.success('批量删除成功')
|
||||
selectedRows.value = []
|
||||
await loadData()
|
||||
emit('changed')
|
||||
}, fullscreenLoadingText.batchDelete)
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
searchForm,
|
||||
() => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
},
|
||||
{ debounce: 300, deep: true }
|
||||
)
|
||||
|
||||
defineExpose({ handleAdd, loadData })
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.type-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,516 +1,80 @@
|
||||
<!-- 细胞产品基础信息维护 · 看板模式 P04 -->
|
||||
<!-- 细胞产品基础信息维护 · 多 Tab 列表 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container cell-product-kanban-page">
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<section class="cell-product-stats">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<img src="/money.png" alt="" />
|
||||
</div>
|
||||
<div class="stat-main">
|
||||
<div class="stat-k">产品价格</div>
|
||||
<div class="stat-v">{{ priceTotal }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<img src="/lx.png" alt="" />
|
||||
</div>
|
||||
<div class="stat-main">
|
||||
<div class="stat-k">产品类型</div>
|
||||
<div class="stat-v">{{ typeTotal }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">
|
||||
<img src="/gg.png" alt="" />
|
||||
</div>
|
||||
<div class="stat-main">
|
||||
<div class="stat-k">产品规格</div>
|
||||
<div class="stat-v">{{ specTotal }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="kanban">
|
||||
<!-- 列 1:产品价格 -->
|
||||
<div class="kanban-col">
|
||||
<div class="kanban-col-hd">
|
||||
<span class="kanban-col-title">
|
||||
细胞产品价格
|
||||
<span class="kanban-count">{{ priceTotal }}</span>
|
||||
</span>
|
||||
<el-button
|
||||
class="btn-main kanban-add-btn"
|
||||
type="primary"
|
||||
@click="priceTabRef?.handleAdd()"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增
|
||||
</el-button>
|
||||
</div>
|
||||
<CellProductPriceTab ref="priceTabRef" layout="kanban" @meta-change="onPriceMetaChange" />
|
||||
</div>
|
||||
|
||||
<!-- 列 2:产品类型 -->
|
||||
<div class="kanban-col">
|
||||
<div class="kanban-col-hd">
|
||||
<span class="kanban-col-title">
|
||||
细胞产品类型
|
||||
<span class="kanban-count">{{ typeTotal }}</span>
|
||||
</span>
|
||||
<el-button class="btn-main kanban-add-btn" type="primary" @click="handleTypeAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="kanban-col-search">
|
||||
<el-input
|
||||
v-model="typeSearchForm.keyword"
|
||||
placeholder="类型编码/名称"
|
||||
clearable
|
||||
@keyup.enter="handleTypeSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="kanban-list" v-loading="typeLoading">
|
||||
<article
|
||||
v-for="row in typeTableData"
|
||||
:key="row.id"
|
||||
class="entity-card"
|
||||
@dblclick="handleTypeView(row)"
|
||||
>
|
||||
<div class="entity-card-del" @click.stop>
|
||||
<el-button class="btn-op-item" title="删除" @click="handleTypeDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="entity-card-body">
|
||||
<h3 class="entity-card-title">{{ row.productTypeName }}</h3>
|
||||
<div class="entity-card-meta">{{ row.productTypeCode }}</div>
|
||||
<div v-if="row.remark" class="entity-card-tags">
|
||||
<span class="tag muted">{{ row.remark }}</span>
|
||||
</div>
|
||||
<div class="entity-card-ft" @click.stop>
|
||||
<div class="entity-card-ft-actions">
|
||||
<el-button class="btn-op-item" @click="handleTypeView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleTypeEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<div v-if="!typeLoading && !typeTableData.length" class="kanban-empty">
|
||||
<span>暂无类型数据</span>
|
||||
<span class="kanban-empty__hint">点击右上角「新增」添加产品类型</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 列 3:产品规格 -->
|
||||
<div class="kanban-col">
|
||||
<div class="kanban-col-hd">
|
||||
<span class="kanban-col-title">
|
||||
细胞产品规格
|
||||
<span class="kanban-count">{{ specTotal }}</span>
|
||||
</span>
|
||||
<el-button
|
||||
class="btn-main kanban-add-btn"
|
||||
type="primary"
|
||||
@click="specTabRef?.handleAdd()"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增
|
||||
</el-button>
|
||||
</div>
|
||||
<CellProductSpecTab ref="specTabRef" layout="kanban" @meta-change="onSpecMetaChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 类型弹窗 -->
|
||||
<el-dialog
|
||||
v-model="typeDialogVisible"
|
||||
:title="typeDialogTitle"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
modal-class="protocol-dialog-mask"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="typeFormRef"
|
||||
:model="typeFormData"
|
||||
:rules="typeFormRules"
|
||||
label-position="top"
|
||||
class="type-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="类型编码" prop="productTypeCode">
|
||||
<el-input v-model="typeFormData.productTypeCode" placeholder="请输入类型编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型名称" prop="productTypeName">
|
||||
<el-input v-model="typeFormData.productTypeName" placeholder="请输入类型名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="typeFormData.remark" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="typeDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
@click="handleTypeSubmit"
|
||||
:loading="typeSubmitLoading"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="typeViewVisible"
|
||||
:title="typeViewRow?.productTypeName || '类型详情'"
|
||||
:icon="Grid"
|
||||
:code="typeViewRow?.productTypeCode"
|
||||
code-label="类型编码"
|
||||
:sections="typeViewSections"
|
||||
/>
|
||||
<el-tabs v-model="activeTab" class="panel-pill-tabs panel-pill-tabs--lg">
|
||||
<el-tab-pane label="细胞产品价格" name="price">
|
||||
<CellProductPriceTab ref="priceTabRef" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="细胞产品类型" name="type" lazy>
|
||||
<CellProductTypeTab @changed="onTypeChanged" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="细胞产品规格" name="spec" lazy>
|
||||
<CellProductSpecTab />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="产品包装" name="package" lazy>
|
||||
<CellProductPackageTab />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check, View, Edit, Delete, Grid } from '@element-plus/icons-vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import {
|
||||
fetchCellProductTypeList,
|
||||
fetchCreateCellProductType,
|
||||
fetchUpdateCellProductType,
|
||||
fetchDeleteCellProductType
|
||||
} from '@/api/admin'
|
||||
import CellProductSpecTab from './CellProductSpecTab.vue'
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import CellProductPriceTab from './CellProductPriceTab.vue'
|
||||
import CellProductTypeTab from './CellProductTypeTab.vue'
|
||||
import CellProductSpecTab from './CellProductSpecTab.vue'
|
||||
import CellProductPackageTab from './CellProductPackageTab.vue'
|
||||
|
||||
defineOptions({ name: 'PilotCellProductType' })
|
||||
|
||||
const KANBAN_PAGE_SIZE = 100
|
||||
const TAB_KEYS = ['price', 'type', 'spec', 'package'] as const
|
||||
type TabKey = (typeof TAB_KEYS)[number]
|
||||
|
||||
const specTabRef = ref<InstanceType<typeof CellProductSpecTab> | null>(null)
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
function normalizeTab(raw: unknown): TabKey {
|
||||
const v = Array.isArray(raw) ? raw[0] : raw
|
||||
if (v === 'type' || v === 'spec' || v === 'price' || v === 'package') return v
|
||||
return 'price'
|
||||
}
|
||||
|
||||
const activeTab = ref<TabKey>(normalizeTab(route.query.tab))
|
||||
const priceTabRef = ref<InstanceType<typeof CellProductPriceTab> | null>(null)
|
||||
|
||||
const specTotal = ref(0)
|
||||
const priceTotal = ref(0)
|
||||
function syncTabToQuery(tab: TabKey) {
|
||||
if (route.query.tab === tab) return
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
tab
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const typeSearchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const typeTableData = ref<Api.CellProductType.CellProductTypeItem[]>([])
|
||||
const typeLoading = ref(false)
|
||||
const typeTotal = ref(0)
|
||||
const typeDialogVisible = ref(false)
|
||||
const typeDialogTitle = ref('新增类型')
|
||||
const typeIsEdit = ref(false)
|
||||
const typeSubmitLoading = ref(false)
|
||||
const typeFormRef = ref()
|
||||
const typeEditingId = ref<string | null>(null)
|
||||
const typeViewVisible = ref(false)
|
||||
const typeViewRow = ref<Api.CellProductType.CellProductTypeItem | null>(null)
|
||||
function onTypeChanged() {
|
||||
priceTabRef.value?.loadData?.()
|
||||
}
|
||||
|
||||
const typeFormData = reactive({
|
||||
productTypeCode: '',
|
||||
productTypeName: '',
|
||||
remark: ''
|
||||
watch(activeTab, (tab) => {
|
||||
syncTabToQuery(tab)
|
||||
})
|
||||
|
||||
const typeFormRules = {
|
||||
productTypeCode: [{ required: true, message: '请输入类型编码', trigger: 'blur' }],
|
||||
productTypeName: [{ required: true, message: '请输入类型名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const typeViewSections = computed(() => {
|
||||
if (!typeViewRow.value) return []
|
||||
const row = typeViewRow.value
|
||||
return [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '类型编码', value: row.productTypeCode },
|
||||
{ label: '类型名称', value: row.productTypeName },
|
||||
{ label: '备注', value: row.remark, span: true }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
function onSpecMetaChange(payload: { total: number }) {
|
||||
specTotal.value = payload.total
|
||||
}
|
||||
|
||||
function onPriceMetaChange(payload: { total: number }) {
|
||||
priceTotal.value = payload.total
|
||||
}
|
||||
|
||||
async function loadTypeData() {
|
||||
typeLoading.value = true
|
||||
try {
|
||||
const res = await fetchCellProductTypeList({
|
||||
page: 1,
|
||||
pageSize: KANBAN_PAGE_SIZE,
|
||||
keyword: typeSearchForm.keyword || undefined
|
||||
})
|
||||
typeTableData.value = res.list || []
|
||||
typeTotal.value = res.total ?? 0
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
watch(
|
||||
() => route.query.tab,
|
||||
(tab) => {
|
||||
const next = normalizeTab(tab)
|
||||
if (activeTab.value !== next) activeTab.value = next
|
||||
}
|
||||
}
|
||||
|
||||
function handleTypeSearch() {
|
||||
loadTypeData()
|
||||
}
|
||||
|
||||
function resetTypeForm() {
|
||||
typeFormData.productTypeCode = ''
|
||||
typeFormData.productTypeName = ''
|
||||
typeFormData.remark = ''
|
||||
typeEditingId.value = null
|
||||
}
|
||||
|
||||
function handleTypeAdd() {
|
||||
typeIsEdit.value = false
|
||||
typeDialogTitle.value = '新增类型'
|
||||
resetTypeForm()
|
||||
typeDialogVisible.value = true
|
||||
typeFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
function handleTypeEdit(row: Api.CellProductType.CellProductTypeItem) {
|
||||
typeIsEdit.value = true
|
||||
typeDialogTitle.value = '编辑类型'
|
||||
typeEditingId.value = row.id
|
||||
typeFormData.productTypeCode = row.productTypeCode || ''
|
||||
typeFormData.productTypeName = row.productTypeName || ''
|
||||
typeFormData.remark = row.remark || ''
|
||||
typeDialogVisible.value = true
|
||||
typeFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
async function handleTypeSubmit() {
|
||||
await typeFormRef.value?.validate?.()
|
||||
typeSubmitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
productTypeCode: typeFormData.productTypeCode.trim(),
|
||||
productTypeName: typeFormData.productTypeName.trim(),
|
||||
remark: typeFormData.remark?.trim() || undefined
|
||||
}
|
||||
if (typeIsEdit.value && typeEditingId.value) {
|
||||
await fetchUpdateCellProductType(typeEditingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateCellProductType(payload)
|
||||
}
|
||||
ElMessage.success(typeIsEdit.value ? '更新成功' : '创建成功')
|
||||
typeDialogVisible.value = false
|
||||
await loadTypeData()
|
||||
priceTabRef.value?.loadData?.()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
typeSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleTypeView(row: Api.CellProductType.CellProductTypeItem) {
|
||||
typeViewRow.value = row
|
||||
typeViewVisible.value = true
|
||||
}
|
||||
|
||||
async function handleTypeDelete(row: Api.CellProductType.CellProductTypeItem) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该类型吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteCellProductType(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadTypeData()
|
||||
priceTabRef.value?.loadData?.()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
() => typeSearchForm.keyword,
|
||||
() => {
|
||||
loadTypeData()
|
||||
},
|
||||
{ debounce: 300 }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
loadTypeData()
|
||||
const next = normalizeTab(route.query.tab)
|
||||
activeTab.value = next
|
||||
syncTabToQuery(next)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cell-product-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 72px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #d1fae5;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, var(--primary-bg, #ecfdf5) 0%, #fff 100%);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-k {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
}
|
||||
|
||||
.stat-v {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-dark, #059669);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.kanban {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.kanban-col {
|
||||
min-width: 0;
|
||||
background: var(--table-head, #f8fafc);
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 280px);
|
||||
max-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.kanban-col-hd {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--mint-border, #d9e8e2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kanban-col-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text-muted, #334155);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kanban-count {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface, #fff);
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
}
|
||||
|
||||
.kanban-add-btn {
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.kanban-col-search {
|
||||
padding: 8px 10px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.kanban-list {
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@import './cell-product-kanban.scss';
|
||||
|
||||
.type-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.kanban {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.kanban-col {
|
||||
min-height: 320px;
|
||||
max-height: 420px;
|
||||
}
|
||||
|
||||
.cell-product-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -528,23 +528,29 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isEdit" class="cell-product-toolbar">
|
||||
<div class="cell-product-toolbar">
|
||||
<div class="section-subtitle">细胞产品</div>
|
||||
<el-button type="primary" :disabled="!canAddCellProduct" @click="addSaleProductLine">
|
||||
<el-button
|
||||
v-if="!isEdit"
|
||||
type="primary"
|
||||
:disabled="!canAddCellProduct"
|
||||
@click="addSaleProductLine"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加产品
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="!isEdit"
|
||||
:data="saleProductLines"
|
||||
stripe
|
||||
class="cell-product-table"
|
||||
style="width: 100%"
|
||||
empty-text="暂无交付计划"
|
||||
>
|
||||
<el-table-column label="细胞产品" min-width="180">
|
||||
<template #default="{ row, $index }">
|
||||
<el-select
|
||||
v-if="!isEdit"
|
||||
v-model="row.priceId"
|
||||
filterable
|
||||
clearable
|
||||
@@ -559,28 +565,55 @@
|
||||
:value="p.priceId"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-else>{{
|
||||
formatSaleProductDisplay(row) || '—'
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规格" min-width="100">
|
||||
<template #default="{ row }">{{ row.specText || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品包装" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="!isEdit"
|
||||
v-model="row.packageId"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择包装"
|
||||
class="cell-product-table__control"
|
||||
@change="onSalePackageSelect(row, $event)"
|
||||
>
|
||||
<el-option
|
||||
v-for="pkg in packageOptions"
|
||||
:key="pkg.id"
|
||||
:label="pkg.packageName"
|
||||
:value="pkg.id"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-else>{{ row.packageText || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参考单价(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyDisplay(row.agreementPrice, 2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="实际单价(元)" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<DecimalInput
|
||||
v-if="!isEdit"
|
||||
v-model="row.actualPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
@change="syncSaleLineAmount(row)"
|
||||
/>
|
||||
<span v-else>{{ formatMoneyDisplay(row.actualPrice, 2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="份数" min-width="90">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-if="!isEdit"
|
||||
v-model="row.quantity"
|
||||
:min="1"
|
||||
:max="9999"
|
||||
@@ -588,30 +621,35 @@
|
||||
style="width: 100%"
|
||||
@change="syncSaleLineAmount(row)"
|
||||
/>
|
||||
<span v-else>{{ row.quantity ?? '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="bizConfig.pickupTimeLabel" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-date-picker
|
||||
v-if="!isEdit"
|
||||
v-model="row.pickupTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择交付时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span v-else>{{ row.pickupTime || '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额(元)" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<DecimalInput
|
||||
v-if="!isEdit"
|
||||
v-model="row.amount"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<span v-else>{{ formatMoneyDisplay(row.amount, 2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="80" align="center">
|
||||
<el-table-column v-if="!isEdit" label="操作" min-width="80" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="removeSaleProductLine($index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -1195,6 +1233,7 @@
|
||||
fetchChannelAgreements,
|
||||
fetchChannelAgreementPrices,
|
||||
fetchCellProductPriceList,
|
||||
fetchCellProductPackageList,
|
||||
fetchDictDataOptions,
|
||||
fetchCustomerList,
|
||||
fetchCreateCustomer
|
||||
@@ -1290,6 +1329,8 @@
|
||||
projectName: string
|
||||
cellTypeName: string
|
||||
specText: string
|
||||
packageId: string
|
||||
packageText: string
|
||||
agreementPrice: number
|
||||
actualPrice: number
|
||||
quantity: number
|
||||
@@ -1299,6 +1340,7 @@
|
||||
|
||||
let saleLineSeq = 0
|
||||
const saleProductLines = ref<SaleProductLine[]>([])
|
||||
const packageOptions = ref<Api.CellProductPackage.CellProductPackageItem[]>([])
|
||||
|
||||
const formRules = reactive({
|
||||
applicationName: [{ required: true, message: '请输入申请单名称', trigger: 'blur' }],
|
||||
@@ -1538,6 +1580,21 @@
|
||||
return !!selectedServiceOrder.value && agreementPriceOptions.value.length > 0
|
||||
})
|
||||
|
||||
const loadPackageOptions = async () => {
|
||||
try {
|
||||
const res: any = await fetchCellProductPackageList({ page: 1, pageSize: 100 })
|
||||
packageOptions.value = res?.list || []
|
||||
} catch {
|
||||
packageOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const onSalePackageSelect = (line: SaleProductLine, packageId: string) => {
|
||||
const pkg = packageOptions.value.find((p) => p.id === packageId)
|
||||
line.packageId = packageId || ''
|
||||
line.packageText = pkg?.packageName || ''
|
||||
}
|
||||
|
||||
const addSaleProductLine = () => {
|
||||
saleProductLines.value.push({
|
||||
_localId: ++saleLineSeq,
|
||||
@@ -1546,6 +1603,8 @@
|
||||
projectName: '',
|
||||
cellTypeName: '',
|
||||
specText: '',
|
||||
packageId: '',
|
||||
packageText: '',
|
||||
agreementPrice: 0,
|
||||
actualPrice: 0,
|
||||
quantity: 1,
|
||||
@@ -2677,6 +2736,13 @@
|
||||
return `${name}(${cellType}、${spec}、¥${price})`
|
||||
}
|
||||
|
||||
const formatSaleProductDisplay = (row: SaleProductLine) => {
|
||||
const name = row.projectName || row.projectCode || ''
|
||||
const cellType = row.cellTypeName || ''
|
||||
if (name && cellType) return `${name}(${cellType})`
|
||||
return name || cellType
|
||||
}
|
||||
|
||||
// ── 弹窗打开 ───────────────────────────────────────────────────────────
|
||||
const handleOpen = () => {
|
||||
resetForm()
|
||||
@@ -2686,6 +2752,9 @@
|
||||
if (isClinical.value || isBasicExperiment.value) {
|
||||
loadClinicalCellProductPrices()
|
||||
}
|
||||
if (isBasicExperiment.value) {
|
||||
loadPackageOptions()
|
||||
}
|
||||
if (isBasicExperiment.value && !isEdit.value) {
|
||||
addSaleProductLine()
|
||||
}
|
||||
@@ -2878,6 +2947,8 @@
|
||||
projectName: cp.projectName || '',
|
||||
cellTypeName: cp.cellTypeName || s.cellType || '',
|
||||
specText: cp.specText || s.specText || '',
|
||||
packageId: cp.packageId || s.packageId || '',
|
||||
packageText: cp.packageText || s.packageText || '',
|
||||
agreementPrice: cp.agreementPrice ?? 0,
|
||||
actualPrice: cp.actualPrice ?? 0,
|
||||
quantity: cp.quantity || 1,
|
||||
@@ -2922,6 +2993,10 @@
|
||||
ElMessage.warning(`第 ${i + 1} 行请选择细胞产品`)
|
||||
return
|
||||
}
|
||||
if (!line.packageId) {
|
||||
ElMessage.warning(`第 ${i + 1} 行请选择产品包装`)
|
||||
return
|
||||
}
|
||||
if (!line.pickupTime) {
|
||||
ElMessage.warning(`第 ${i + 1} 行请填写提货时间`)
|
||||
return
|
||||
@@ -3030,6 +3105,8 @@
|
||||
projectName: line.projectName,
|
||||
cellTypeName: line.cellTypeName,
|
||||
specText: line.specText,
|
||||
packageId: line.packageId,
|
||||
packageText: line.packageText,
|
||||
agreementPrice: line.agreementPrice,
|
||||
actualPrice: line.actualPrice,
|
||||
quantity: line.quantity || 1,
|
||||
|
||||
@@ -234,7 +234,10 @@
|
||||
: '质检工单派发成功'
|
||||
)
|
||||
emit('success', { sampleCode: props.workOrderRow.sampleCode })
|
||||
handleClose()
|
||||
resetForm()
|
||||
if (productionTasks.value.length === 1) {
|
||||
form.taskId = productionTasks.value[0].taskId
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
|
||||
+85
-23
@@ -72,16 +72,16 @@
|
||||
>
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canSaveTaskSet(row)"
|
||||
class="btn-op-item"
|
||||
@click="emit('command', 'save-set', row)"
|
||||
>
|
||||
<el-icon><FolderAdd /></el-icon>保存为模版
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="emit('view-qc', row)">
|
||||
<el-icon><DataLine /></el-icon>查看质检数据
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showStorageCertQcButton(row)"
|
||||
class="btn-op-item"
|
||||
@click="openStorageCertQcDialog(row)"
|
||||
>
|
||||
<el-icon><Document /></el-icon>填写储存证质检信息
|
||||
</el-button>
|
||||
<el-tooltip
|
||||
v-if="!isMineMode && rowCardMeta(row).allowDispatchQc"
|
||||
:content="dispatchQcTooltip(row)"
|
||||
@@ -112,32 +112,42 @@
|
||||
>
|
||||
<el-icon><Check /></el-icon>完成工单
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showCardComplete(row) && canVoidWorkOrder(row)"
|
||||
class="btn-op-item"
|
||||
type="danger"
|
||||
plain
|
||||
@click="emit('void', row)"
|
||||
>
|
||||
<el-icon><Close /></el-icon>作废工单
|
||||
</el-button>
|
||||
<el-dropdown
|
||||
v-if="!isMineMode && hasMoreActions(row)"
|
||||
v-if="hasMoreActions(row)"
|
||||
trigger="hover"
|
||||
@command="(cmd) => emit('command', cmd, row)"
|
||||
@command="(cmd) => handleMoreCommand(cmd, row)"
|
||||
>
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="canDispatchTask(row)" command="view">
|
||||
<el-dropdown-item
|
||||
v-if="!isMineMode && canDispatchTask(row)"
|
||||
command="view"
|
||||
>
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canEditWorkOrder(row)" command="edit">
|
||||
<el-dropdown-item
|
||||
v-if="!isMineMode && canEditWorkOrder(row)"
|
||||
command="edit"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="canDeleteWorkOrder(row)" command="delete" divided>
|
||||
<el-dropdown-item v-if="canSaveTaskSet(row)" command="save-set">
|
||||
<el-icon><FolderAdd /></el-icon>保存为模版
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="showCardComplete(row) && canVoidWorkOrder(row)"
|
||||
command="void"
|
||||
>
|
||||
<el-icon><Close /></el-icon>作废工单
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="!isMineMode && canDeleteWorkOrder(row)"
|
||||
command="delete"
|
||||
divided
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -201,6 +211,10 @@
|
||||
<dt>规格</dt>
|
||||
<dd>{{ product.specText || row.specText || '—' }}</dd>
|
||||
</div>
|
||||
<div class="production-work-order-card__cell-field">
|
||||
<dt>产品包装</dt>
|
||||
<dd>{{ product.packageText || row.packageText || '—' }}</dd>
|
||||
</div>
|
||||
<div class="production-work-order-card__cell-field">
|
||||
<dt>数量</dt>
|
||||
<dd>{{ product.quantity != null ? product.quantity : '—' }}</dd>
|
||||
@@ -222,10 +236,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="production-work-order-card__cells-fallback">
|
||||
<template v-if="row.cellType || row.specText">
|
||||
<template v-if="row.cellType || row.specText || row.packageText">
|
||||
<span v-if="row.cellType">细胞类型:{{ row.cellType }}</span>
|
||||
<span v-if="row.cellType && row.specText"> · </span>
|
||||
<span v-if="row.specText">规格:{{ row.specText }}</span>
|
||||
<span v-if="(row.cellType || row.specText) && row.packageText"> · </span>
|
||||
<span v-if="row.packageText">包装:{{ row.packageText }}</span>
|
||||
</template>
|
||||
<template v-else>暂无细胞产品明细</template>
|
||||
</p>
|
||||
@@ -297,6 +313,12 @@
|
||||
@current-change="(v) => emit('page-change', v)"
|
||||
/>
|
||||
</div>
|
||||
<StorageCertQcDialog
|
||||
v-model:visible="storageCertQcVisible"
|
||||
mode="edit"
|
||||
:sample-code="storageCertQcSampleCode"
|
||||
:sample-type-name="storageCertQcSampleTypeName"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -320,6 +342,8 @@
|
||||
import type { OutboundQcDetailProduct } from '@/api/qc-work-order'
|
||||
import type { ProductionWorkOrderCardMetaConfig } from '@/utils/production-work-order-card-meta'
|
||||
import SampleCodeWithSpecialDisplay from '@/components/business/sample-code-with-special-display/index.vue'
|
||||
import StorageCertQcDialog from '@/components/business/storage-cert-qc-dialog/index.vue'
|
||||
import { supportsStorageCertQc } from '@/utils/storage-cert-qc-fields'
|
||||
import {
|
||||
getCardMetaConfigForLegacyTab,
|
||||
resolveCardMetaForRow
|
||||
@@ -384,6 +408,26 @@
|
||||
}
|
||||
|
||||
const expandedFeeIds = ref<Set<string>>(new Set())
|
||||
const storageCertQcVisible = ref(false)
|
||||
const storageCertQcSampleCode = ref('')
|
||||
const storageCertQcSampleTypeName = ref('')
|
||||
|
||||
function showStorageCertQcButton(row: ProductionWorkOrderItem) {
|
||||
const tabKey = resolveEffectiveRowTabKey(listTabKey.value, row)
|
||||
if (tabKey !== 'inbound_cell_storage') return false
|
||||
return supportsStorageCertQc(String(row.sampleTypeName || ''))
|
||||
}
|
||||
|
||||
function openStorageCertQcDialog(row: ProductionWorkOrderItem) {
|
||||
const code = String(row.sampleCode || '').trim()
|
||||
if (!code) {
|
||||
ElMessage.warning('缺少样本编码,无法填写')
|
||||
return
|
||||
}
|
||||
storageCertQcSampleCode.value = code
|
||||
storageCertQcSampleTypeName.value = String(row.sampleTypeName || '')
|
||||
storageCertQcVisible.value = true
|
||||
}
|
||||
|
||||
function isFeeExpanded(id: string) {
|
||||
return expandedFeeIds.value.has(id)
|
||||
@@ -573,7 +617,25 @@
|
||||
}
|
||||
|
||||
function hasMoreActions(row: ProductionWorkOrderItem) {
|
||||
return canDispatchTask(row) || canEditWorkOrder(row) || canDeleteWorkOrder(row) || true
|
||||
if (isMineMode.value) {
|
||||
return showCardComplete(row) && canVoidWorkOrder(row)
|
||||
}
|
||||
return (
|
||||
canDispatchTask(row) ||
|
||||
canEditWorkOrder(row) ||
|
||||
canSaveTaskSet(row) ||
|
||||
(showCardComplete(row) && canVoidWorkOrder(row)) ||
|
||||
canDeleteWorkOrder(row) ||
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
function handleMoreCommand(cmd: string, row: ProductionWorkOrderItem) {
|
||||
if (cmd === 'void') {
|
||||
emit('void', row)
|
||||
return
|
||||
}
|
||||
emit('command', cmd, row)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@
|
||||
<!-- 搜索框 -->
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索产品编码或名称"
|
||||
placeholder="搜索产品编码、名称或储物格位置"
|
||||
clearable
|
||||
style="margin-bottom: 12px"
|
||||
@input="handleSearch"
|
||||
@@ -402,6 +402,7 @@
|
||||
(cp) =>
|
||||
(cp.productCode || '').toLowerCase().includes(keyword) ||
|
||||
(cp.productName || '').toLowerCase().includes(keyword) ||
|
||||
(cp.storageSlotName || '').toLowerCase().includes(keyword) ||
|
||||
(cp.label || '').toLowerCase().includes(keyword)
|
||||
)
|
||||
})
|
||||
|
||||
@@ -44,16 +44,6 @@
|
||||
}}</span>
|
||||
<div class="work-order-task-step-sublist__ops">
|
||||
<el-button
|
||||
v-if="canEnterStep(row)"
|
||||
class="btn-op-item primary"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click.stop="handleEnterStep(row)"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon>进入执行
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
class="btn-op-item primary"
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -85,7 +75,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { VideoPlay, View } from '@element-plus/icons-vue'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchProductionTaskDetail,
|
||||
type ProductionTaskOperateScope
|
||||
@@ -128,7 +118,7 @@
|
||||
const tableData = ref<Record<string, any>[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
/** 我的生产任务:展示任务下全部工艺步骤;本人负责步骤高亮,进入执行仍仅限本人步骤 */
|
||||
/** 我的生产任务:展示任务下全部工艺步骤;本人负责步骤高亮 */
|
||||
const visibleSteps = computed(() => tableData.value)
|
||||
|
||||
function assigneeDisplayName(raw: string): string {
|
||||
@@ -176,12 +166,6 @@
|
||||
return (row.assigneeUserIds || []).includes(uid)
|
||||
}
|
||||
|
||||
function canEnterStep(row: Record<string, any>) {
|
||||
if (row.stepStatus === 'ywc') return false
|
||||
if (isMineMode.value) return isStepAssignee(row)
|
||||
return true
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const taskId = props.task?.id
|
||||
if (!taskId) {
|
||||
@@ -224,10 +208,6 @@
|
||||
executionDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEnterStep(row: Record<string, any>) {
|
||||
openExecution(row, false)
|
||||
}
|
||||
|
||||
function handleViewStep(row: Record<string, any>) {
|
||||
openExecution(row, true)
|
||||
}
|
||||
|
||||
+8
@@ -65,6 +65,14 @@
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="packageText"
|
||||
label="产品包装"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="quantity"
|
||||
label="数量"
|
||||
|
||||
@@ -101,6 +101,10 @@
|
||||
<dt>规格</dt>
|
||||
<dd>{{ product.specText || row.specText || '—' }}</dd>
|
||||
</div>
|
||||
<div class="qc-work-order-card__cell-field">
|
||||
<dt>产品包装</dt>
|
||||
<dd>{{ product.packageText || row.packageText || '—' }}</dd>
|
||||
</div>
|
||||
<div class="qc-work-order-card__cell-field">
|
||||
<dt>数量</dt>
|
||||
<dd>{{ product.quantity != null ? product.quantity : '—' }}</dd>
|
||||
@@ -122,10 +126,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="qc-work-order-card__cells-fallback">
|
||||
<template v-if="row.cellType || row.specText">
|
||||
<template v-if="row.cellType || row.specText || row.packageText">
|
||||
<span v-if="row.cellType">细胞类型:{{ row.cellType }}</span>
|
||||
<span v-if="row.cellType && row.specText"> · </span>
|
||||
<span v-if="row.specText">规格:{{ row.specText }}</span>
|
||||
<span v-if="(row.cellType || row.specText) && row.packageText"> · </span>
|
||||
<span v-if="row.packageText">包装:{{ row.packageText }}</span>
|
||||
</template>
|
||||
<template v-else>暂无细胞产品明细</template>
|
||||
</p>
|
||||
@@ -339,9 +345,12 @@
|
||||
const sampleSpecialDialogVisible = ref(false)
|
||||
const storageCertQcVisible = ref(false)
|
||||
|
||||
const showStorageCertQcButton = computed(() =>
|
||||
supportsStorageCertQc(String(props.row?.sampleTypeName || ''))
|
||||
)
|
||||
const showStorageCertQcButton = computed(() => {
|
||||
// 科研捐赠质检不填写储存证质检信息(质检工单 / 我的质检任务共用本卡片)
|
||||
const tabKey = resolveEffectiveRowTabKey(props.cardMetaConfig.tabKey, props.row)
|
||||
if (tabKey === 'inbound_research_donation') return false
|
||||
return supportsStorageCertQc(String(props.row?.sampleTypeName || ''))
|
||||
})
|
||||
|
||||
const sampleSpecialMarkTarget = computed(() =>
|
||||
resolveInboundSampleSpecialMarkTargetFromRow(props.row)
|
||||
|
||||
+23
@@ -7,6 +7,7 @@ import {
|
||||
groupMineTasksByWorkOrder,
|
||||
filterMineEntriesByWorkOrderSearch,
|
||||
sortMineEntriesForInboundLabStorage,
|
||||
sortMineEntriesForTemporary,
|
||||
paginateMineEntries,
|
||||
buildMineTableRows
|
||||
} from '@/utils/qc-mine-task-groups'
|
||||
@@ -23,6 +24,10 @@ import {
|
||||
} from '@/utils/qc-work-order-tabs'
|
||||
import { toAllTabListQuery } from '@/utils/qc-work-order-category'
|
||||
import { getCardMetaConfig } from '@/utils/qc-work-order-card-meta'
|
||||
import {
|
||||
DEFAULT_DONATION_PROTOCOL_STATUS_FILTER,
|
||||
joinProtocolStatusFilter
|
||||
} from '@/utils/donation-protocol-status-filter'
|
||||
import { joinWorkOrderStatusFilter } from '@/utils/status-tag-registry'
|
||||
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||
import { fetchQcInspectionBatchesBatch, type QcInspectionBatch } from '@/utils/qc-inspection-batch'
|
||||
@@ -52,6 +57,7 @@ export function useQcWorkOrderTabPage(
|
||||
qcCategory: '',
|
||||
keyword: '',
|
||||
workOrderStatus: [...DEFAULT_WORK_ORDER_STATUS_FILTER],
|
||||
protocolStatus: [...DEFAULT_DONATION_PROTOCOL_STATUS_FILTER],
|
||||
triggerSource: '',
|
||||
sampleTypeId: '',
|
||||
taskStatus: '',
|
||||
@@ -122,6 +128,10 @@ export function useQcWorkOrderTabPage(
|
||||
workOrderStatus: searchForm.workOrderStatus.length
|
||||
? [...searchForm.workOrderStatus]
|
||||
: undefined,
|
||||
protocolStatus:
|
||||
tabKey.value === 'inbound_research_donation'
|
||||
? [...searchForm.protocolStatus]
|
||||
: undefined,
|
||||
qcCategory: searchForm.qcCategory || undefined,
|
||||
triggerSource:
|
||||
shouldApplyTriggerSourceFilter(tabKey.value, tab.listLayout) && searchForm.triggerSource
|
||||
@@ -148,6 +158,8 @@ export function useQcWorkOrderTabPage(
|
||||
entries = filterMineEntriesByWorkOrderSearch(entries, buildMineWorkOrderSearchFilters())
|
||||
if (isInboundSampleQcTab(tabKey.value)) {
|
||||
entries = sortMineEntriesForInboundLabStorage(entries)
|
||||
} else if (tabKey.value === 'temporary') {
|
||||
entries = sortMineEntriesForTemporary(entries)
|
||||
}
|
||||
const pageResult = paginateMineEntries(entries, pagination.page, pagination.pageSize)
|
||||
const tableModel = buildMineTableRows(pageResult.items)
|
||||
@@ -159,6 +171,12 @@ export function useQcWorkOrderTabPage(
|
||||
}
|
||||
|
||||
const tab = tabConfig.value
|
||||
if (tabKey.value === 'inbound_research_donation' && !searchForm.protocolStatus.length) {
|
||||
tableData.value = []
|
||||
presetBatchesByWorkOrderId.value = undefined
|
||||
pagination.total = 0
|
||||
return
|
||||
}
|
||||
const timeParams =
|
||||
isAllQcWorkOrderTab(tabKey.value) || !tabUsesInfusionTimeFilter(tab)
|
||||
? {
|
||||
@@ -177,6 +195,10 @@ export function useQcWorkOrderTabPage(
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
workOrderStatus: joinWorkOrderStatusFilter(searchForm.workOrderStatus),
|
||||
protocolStatus:
|
||||
tabKey.value === 'inbound_research_donation'
|
||||
? joinProtocolStatusFilter(searchForm.protocolStatus)
|
||||
: undefined,
|
||||
triggerSource:
|
||||
shouldApplyTriggerSourceFilter(tabKey.value, tab.listLayout) && searchForm.triggerSource
|
||||
? searchForm.triggerSource
|
||||
@@ -211,6 +233,7 @@ export function useQcWorkOrderTabPage(
|
||||
searchForm.qcCategory = ''
|
||||
searchForm.keyword = ''
|
||||
searchForm.workOrderStatus = [...DEFAULT_WORK_ORDER_STATUS_FILTER]
|
||||
searchForm.protocolStatus = [...DEFAULT_DONATION_PROTOCOL_STATUS_FILTER]
|
||||
searchForm.triggerSource = ''
|
||||
searchForm.sampleTypeId = ''
|
||||
searchForm.taskStatus = ''
|
||||
|
||||
@@ -40,6 +40,22 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="tabKey === 'inbound_research_donation'" label="协议状态">
|
||||
<el-select
|
||||
v-model="searchForm.protocolStatus"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in DONATION_PROTOCOL_STATUS_FILTER_OPTIONS"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="!isAllTab && (tabConfig.listLayout !== 'standalone' || tabKey === 'temporary')"
|
||||
label="工单来源"
|
||||
@@ -170,6 +186,22 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="tabKey === 'inbound_research_donation'" label="协议状态">
|
||||
<el-select
|
||||
v-model="searchForm.protocolStatus"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
multiple
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in DONATION_PROTOCOL_STATUS_FILTER_OPTIONS"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务状态">
|
||||
<el-select
|
||||
v-model="searchForm.taskStatus"
|
||||
@@ -359,6 +391,7 @@
|
||||
QC_WORK_ORDER_CATEGORY_OPTIONS,
|
||||
QC_ALL_TAB_TRIGGER_SOURCE_OPTIONS
|
||||
} from '@/utils/qc-work-order-category'
|
||||
import { DONATION_PROTOCOL_STATUS_FILTER_OPTIONS } from '@/utils/donation-protocol-status-filter'
|
||||
import { QC_WORK_ORDER_STATUS_FILTER_OPTIONS } from '@/utils/status-tag-registry'
|
||||
import QcWorkOrderCardList from '../QcWorkOrderCardList.vue'
|
||||
import { QC_WORK_ORDER_VIEW_DIALOG_COMPONENTS } from '../view-dialogs/view-dialog-registry'
|
||||
|
||||
+8
@@ -40,6 +40,14 @@
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="packageText"
|
||||
label="产品包装"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="quantity"
|
||||
label="数量"
|
||||
|
||||
Reference in new issue
Block a user