This commit is contained in:
zlwu committed 2026-05-26 19:04:27 +08:00
1 parent 4a00f21bfb
commit edf88dc016
1503 files changed
+329728 -41987

No files matched your search

+138 -140
View File
@@ -8,9 +8,7 @@
<img class="logo-img" src="/logo.png" alt="中科兴皖" />
</div>
<h1 class="slogan">一体化管理门户</h1>
<p class="description">
安全 · 高效 · 可扩展
</p>
<p class="description"> 安全 · 高效 · 可扩展 </p>
<div class="features">
<div class="feature-item">
<i class="el-icon-setting"></i>
@@ -157,154 +155,154 @@
</script>
<style lang="scss" scoped>
.login-container {
display: flex;
width: 100%;
height: 100vh;
background: #f5f7fa;
}
.login-left {
flex: 1;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
@media (max-width: 768px) {
display: none;
}
}
.brand-content {
color: #fff;
text-align: center;
max-width: 400px;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 48px;
}
.logo-img {
width: min(200px, 55vw);
height: auto;
object-fit: contain;
border-radius: 12px;
background: #fff;
padding: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
.slogan {
font-size: 32px;
font-weight: 600;
margin: 0 0 16px 0;
}
.description {
font-size: 18px;
opacity: 0.9;
margin: 0 0 48px 0;
}
.features {
display: flex;
justify-content: center;
gap: 40px;
}
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
i {
font-size: 32px;
}
span {
font-size: 14px;
opacity: 0.9;
}
}
.login-right {
width: 480px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
padding: 40px;
@media (max-width: 768px) {
.login-container {
display: flex;
width: 100%;
height: 100vh;
background: #f5f7fa;
}
}
.form-container {
width: 100%;
max-width: 360px;
}
.login-left {
flex: 1;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
.form-header {
margin-bottom: 40px;
text-align: center;
@media (max-width: 768px) {
display: none;
}
}
h2 {
font-size: 28px;
.brand-content {
color: #fff;
text-align: center;
max-width: 400px;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 48px;
}
.logo-img {
width: min(200px, 55vw);
height: auto;
object-fit: contain;
border-radius: 12px;
background: #fff;
padding: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
.slogan {
font-size: 32px;
font-weight: 600;
color: #303133;
margin: 0 0 12px 0;
margin: 0 0 16px 0;
}
p {
font-size: 14px;
color: #909399;
margin: 0;
}
}
.login-form {
:deep(.el-form-item) {
margin-bottom: 24px;
}
:deep(.el-input__wrapper) {
padding: 8px 16px;
border-radius: 8px;
}
:deep(.el-input__inner) {
height: 44px;
}
}
.login-btn {
width: 100%;
height: 48px;
font-size: 16px;
border-radius: 8px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none;
&:hover {
.description {
font-size: 18px;
opacity: 0.9;
margin: 0 0 48px 0;
}
}
.form-footer {
margin-top: 40px;
text-align: center;
.features {
display: flex;
justify-content: center;
gap: 40px;
}
.copyright {
font-size: 12px;
color: #c0c4cc;
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
i {
font-size: 32px;
}
span {
font-size: 14px;
opacity: 0.9;
}
}
.login-right {
width: 480px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
padding: 40px;
@media (max-width: 768px) {
width: 100%;
}
}
.form-container {
width: 100%;
max-width: 360px;
}
.form-header {
margin-bottom: 40px;
text-align: center;
h2 {
font-size: 28px;
font-weight: 600;
color: #303133;
margin: 0 0 12px 0;
}
p {
font-size: 14px;
color: #909399;
margin: 0;
}
}
.login-form {
:deep(.el-form-item) {
margin-bottom: 24px;
}
:deep(.el-input__wrapper) {
padding: 8px 16px;
border-radius: 8px;
}
:deep(.el-input__inner) {
height: 44px;
}
}
.login-btn {
width: 100%;
height: 48px;
font-size: 16px;
border-radius: 8px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: none;
&:hover {
opacity: 0.9;
}
}
.form-footer {
margin-top: 40px;
text-align: center;
.copyright {
font-size: 12px;
color: #c0c4cc;
}
}
}
</style>
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编码、名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -115,7 +119,9 @@
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
</template>
@@ -210,7 +216,15 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Collection, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
Collection,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchCellSampleTypeList,
@@ -408,6 +422,5 @@
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
@@ -38,8 +38,12 @@
<div class="filter-actions-row">
<span class="filter-hint">模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -165,7 +169,9 @@
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
</template>
@@ -229,9 +235,7 @@
/>
</el-form-item>
<el-form-item label="序号位数" prop="digitCount">
<template #label
>序号位数<span class="form-label-tip">补零后的总位数</span></template
>
<template #label>序号位数<span class="form-label-tip">补零后的总位数</span></template>
<el-input
type="number"
step="any"
@@ -251,7 +255,12 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="batchGenLoading" @click="handleBatchGenerate">
<el-button
class="btn-main"
type="primary"
:loading="batchGenLoading"
@click="handleBatchGenerate"
>
<el-icon><Check /></el-icon>
生成
</el-button>
@@ -301,7 +310,12 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="batchIssueLoading" @click="handleBatchIssue">
<el-button
class="btn-main"
type="primary"
:loading="batchIssueLoading"
@click="handleBatchIssue"
>
<el-icon><Check /></el-icon>
确定下发
</el-button>
@@ -468,14 +482,14 @@
middle: '34',
seqStart: 1,
seqEnd: 100,
digitCount: 6,
digitCount: 6
})
const batchGenRules = {
prefix: [{ required: true, message: '请输入固定前缀', trigger: 'blur' }],
middle: [{ required: true, message: '请输入中间段', trigger: 'blur' }],
seqStart: [{ required: true, message: '请输入开始序号', trigger: 'blur' }],
seqEnd: [{ required: true, message: '请输入结束序号', trigger: 'blur' }],
digitCount: [{ required: true, message: '请输入序号位数', trigger: 'blur' }],
digitCount: [{ required: true, message: '请输入序号位数', trigger: 'blur' }]
}
const batchGenPreview = computed(() => {
const p = batchGenForm.prefix || ''
@@ -494,10 +508,10 @@
const batchIssueLoading = ref(false)
const batchIssueFormRef = ref()
const batchIssueForm = reactive({
issuedUserId: '',
issuedUserId: ''
})
const batchIssueRules = {
issuedUserId: [{ required: true, message: '请选择下发用户', trigger: 'change' }],
issuedUserId: [{ required: true, message: '请选择下发用户', trigger: 'change' }]
}
const userOptions = ref<Array<{ label: string; value: string }>>([])
@@ -698,7 +712,7 @@
middle: batchGenForm.middle.trim(),
seqStart: batchGenForm.seqStart,
seqEnd: batchGenForm.seqEnd,
digitCount: batchGenForm.digitCount,
digitCount: batchGenForm.digitCount
})
batchGenVisible.value = false
await loadData()
@@ -719,12 +733,12 @@
await batchIssueFormRef.value?.validate?.()
batchIssueLoading.value = true
try {
const ids = selectedRows.value.map(r => r.id)
const userOpt = userOptions.value.find(u => u.value === batchIssueForm.issuedUserId)
const ids = selectedRows.value.map((r) => r.id)
const userOpt = userOptions.value.find((u) => u.value === batchIssueForm.issuedUserId)
await fetchBatchIssueProtocolNos({
ids,
issuedUserId: batchIssueForm.issuedUserId,
issuedUserName: userOpt?.label || undefined,
issuedUserName: userOpt?.label || undefined
})
batchIssueVisible.value = false
selectedRows.value = []
@@ -763,6 +777,5 @@
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按地区名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -84,11 +88,7 @@
>
<template #default="{ row }">
<template v-if="row.hospitalNames && row.hospitalNames.length">
<div
v-for="(name, idx) in row.hospitalNames"
:key="idx"
class="hospital-cell-line"
>
<div v-for="(name, idx) in row.hospitalNames" :key="idx" class="hospital-cell-line">
{{ name }}
</div>
</template>
@@ -123,7 +123,9 @@
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="openHospitalDialog(row)"
>地区医院配置</el-button
>
@@ -415,9 +417,7 @@
const row = viewRow.value
if (!row) return []
const hospitals =
row.hospitalNames && row.hospitalNames.length > 0
? row.hospitalNames.join('、')
: '无'
row.hospitalNames && row.hospitalNames.length > 0 ? row.hospitalNames.join('、') : '无'
return [
{
title: '基本信息',
@@ -670,7 +670,6 @@
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.hospital-dialog-head {
@@ -37,8 +37,12 @@
<div class="filter-actions-row">
<span class="filter-hint">支持模糊查询;默认展示近 30 天内创建的客户。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -90,7 +94,13 @@
align="center"
header-align="center"
/>
<el-table-column prop="sex" label="性别" min-width="72" align="center" header-align="center">
<el-table-column
prop="sex"
label="性别"
min-width="72"
align="center"
header-align="center"
>
<template #default="{ row }">{{ sexLabel(row.sex) }}</template>
</el-table-column>
<el-table-column
@@ -135,7 +145,9 @@
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
</template>
@@ -459,6 +471,5 @@
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
@@ -68,21 +68,67 @@
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="companyName" label="保险公司名称" min-width="160" align="center" header-align="center" />
<el-table-column prop="companyAddress" label="地址" min-width="180" align="center" header-align="center" />
<el-table-column prop="contactName" label="联系人" min-width="100" align="center" header-align="center" />
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" header-align="center" />
<el-table-column prop="tag" label="状态" min-width="90" align="center" header-align="center">
<el-table-column
prop="companyName"
label="保险公司名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="companyAddress"
label="地址"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
prop="contactName"
label="联系人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="contactPhone"
label="联系电话"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="tag"
label="状态"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{ row.status == '1' ? '正常' : '停用' }}</el-tag>
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{
row.status == '1' ? '正常' : '停用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="400" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="400"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item primary" @click="openItemDialog(row)"><el-icon><Setting /></el-icon>险种价格配置</el-button>
<el-button class="btn-op-item" @click="handleEdit(row)"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)"><el-icon><Delete /></el-icon>删除</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="openItemDialog(row)"
><el-icon><Setting /></el-icon>险种价格配置</el-button
>
<el-button class="btn-op-item" @click="handleEdit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDelete(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -109,7 +155,13 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form ref="companyFormRef" :model="companyForm" :rules="companyFormRules" label-position="top" class="biz-dialog-form">
<el-form
ref="companyFormRef"
:model="companyForm"
:rules="companyFormRules"
label-position="top"
class="biz-dialog-form"
>
<div class="form-grid-2">
<el-form-item label="保险公司名称" prop="companyName">
<el-input v-model="companyForm.companyName" placeholder="请输入保险公司名称" />
@@ -155,7 +207,12 @@
<el-button class="btn-ghost" @click="companyDialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="companySubmitLoading" @click="submitCompany">
<el-button
class="btn-main"
type="primary"
:loading="companySubmitLoading"
@click="submitCompany"
>
<el-icon><Check /></el-icon>确定
</el-button>
</template>
@@ -197,21 +254,66 @@
@selection-change="onItemSelectionChange"
>
<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" />
<el-table-column prop="projectName" label="险种名称" min-width="160" align="center" header-align="center" />
<el-table-column prop="projectFee" label="险种费用(元)" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ row.projectFee != null ? '¥' + formatMoneyDisplay(row.projectFee) : '—' }}</template>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="险种名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="projectFee"
label="险种费用(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.projectFee != null ? '¥' + formatMoneyDisplay(row.projectFee) : '—'
}}</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="90" align="center" header-align="center">
<el-table-column
prop="status"
label="状态"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{ row.status == '1' ? '正常' : '停用' }}</el-tag>
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{
row.status == '1' ? '正常' : '停用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" align="center" header-align="center" show-overflow-tooltip />
<el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEditItem(row)"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button class="btn-op-item" @click="handleDeleteItem(row)"><el-icon><Delete /></el-icon>删除</el-button>
<el-button class="btn-op-item primary" @click="handleEditItem(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDeleteItem(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -222,7 +324,9 @@
:total="itemPagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(v) => ((itemPagination.page = 1), (itemPagination.pageSize = v), loadItems())"
@size-change="
(v) => ((itemPagination.page = 1), (itemPagination.pageSize = v), loadItems())
"
@current-change="(v) => ((itemPagination.page = v), loadItems())"
/>
</div>
@@ -239,7 +343,13 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form ref="itemFormRef" :model="itemForm" :rules="itemFormRules" label-position="top" class="biz-dialog-form">
<el-form
ref="itemFormRef"
:model="itemForm"
:rules="itemFormRules"
label-position="top"
class="biz-dialog-form"
>
<div class="form-grid-2">
<el-form-item label="险种名称" prop="projectName">
<el-input v-model="itemForm.projectName" placeholder="请输入险种名称" />
@@ -262,7 +372,12 @@
<el-button class="btn-ghost" @click="itemFormVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="itemSubmitLoading" @click="submitItem">
<el-button
class="btn-main"
type="primary"
:loading="itemSubmitLoading"
@click="submitItem"
>
<el-icon><Check /></el-icon>确定
</el-button>
</template>
@@ -287,7 +402,18 @@
</template>
<script setup lang="ts">
import { Plus, Close, Check, Search, RefreshLeft, View, Setting, Edit, Delete, OfficeBuilding } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
Search,
RefreshLeft,
View,
Setting,
Edit,
Delete,
OfficeBuilding
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import {
@@ -515,7 +641,7 @@
invoiceTitle: companyForm.invoiceTitle?.trim() || undefined,
taxNo: companyForm.taxNo?.trim() || undefined,
bankName: companyForm.bankName?.trim() || undefined,
bankAccount: companyForm.bankAccount?.trim() || undefined,
bankAccount: companyForm.bankAccount?.trim() || undefined
}
await fetchCreateInsuranceCompanyInvoice((newInst as any).id, invoicePayload)
}
@@ -530,9 +656,13 @@
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(`确定删除「${row.companyName}」?其下属险种价格将一并删除。`, '提示', {
type: 'warning'
})
await ElMessageBox.confirm(
`确定删除「${row.companyName}」?其下属险种价格将一并删除。`,
'提示',
{
type: 'warning'
}
)
} catch {
return
}
@@ -556,7 +686,11 @@
{ label: '公司地址', value: row.companyAddress, span: true },
{ label: '联系人', value: row.contactName },
{ label: '联系电话', value: row.contactPhone },
{ label: '状态', value: row.status == '1' ? '正常' : '停用', tagType: row.status == '1' ? 'success' : 'info' },
{
label: '状态',
value: row.status == '1' ? '正常' : '停用',
tagType: row.status == '1' ? 'success' : 'info'
},
{ label: '备注', value: row.remark, span: true }
]
}
@@ -49,8 +49,12 @@
<div class="filter-actions-row">
<span class="filter-hint">模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -185,7 +189,9 @@
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
</template>
@@ -223,7 +229,11 @@
>
<div class="form-grid-2">
<el-form-item label="编码类型" prop="sampleCodeType">
<el-select v-model="batchGenForm.sampleCodeType" placeholder="请选择" style="width: 100%">
<el-select
v-model="batchGenForm.sampleCodeType"
placeholder="请选择"
style="width: 100%"
>
<el-option label="造血编码 (ZKZX)" value="zxbm" />
<el-option label="脐带编码 (ZKQD)" value="qdbm" />
</el-select>
@@ -252,9 +262,7 @@
/>
</el-form-item>
<el-form-item label="序号位数" prop="digitCount">
<template #label
>序号位数<span class="form-label-tip">补零后的总位数</span></template
>
<template #label>序号位数<span class="form-label-tip">补零后的总位数</span></template>
<el-input
type="number"
step="any"
@@ -274,7 +282,12 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="batchGenLoading" @click="handleBatchGenerate">
<el-button
class="btn-main"
type="primary"
:loading="batchGenLoading"
@click="handleBatchGenerate"
>
<el-icon><Check /></el-icon>
生成
</el-button>
@@ -324,7 +337,12 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="batchIssueLoading" @click="handleBatchIssue">
<el-button
class="btn-main"
type="primary"
:loading="batchIssueLoading"
@click="handleBatchIssue"
>
<el-icon><Check /></el-icon>
确定下发
</el-button>
@@ -357,17 +375,15 @@
/>
</el-form-item>
<el-form-item v-if="!isEdit" label="编码类型" prop="sampleCodeType">
<el-select
v-model="formData.sampleCodeType"
placeholder="请选择"
style="width: 100%"
>
<el-select v-model="formData.sampleCodeType" placeholder="请选择" style="width: 100%">
<el-option label="造血编码 (ZKZX)" value="zxbm" />
<el-option label="脐带编码 (ZKQD)" value="qdbm" />
</el-select>
</el-form-item>
<el-form-item v-else label="编码类型">
<span class="text-sm text-gray-500">{{ sampleCodeTypeLabel(formData.sampleCodeType) }}</span>
<span class="text-sm text-gray-500">{{
sampleCodeTypeLabel(formData.sampleCodeType)
}}</span>
</el-form-item>
<el-form-item label="是否下发">
<el-switch
@@ -507,14 +523,14 @@
middle: '34',
seqStart: 1,
seqEnd: 100,
digitCount: 6,
digitCount: 6
})
const batchGenRules = {
sampleCodeType: [{ required: true, message: '请选择编码类型', trigger: 'change' }],
middle: [{ required: true, message: '请输入中间段', trigger: 'blur' }],
seqStart: [{ required: true, message: '请输入开始序号', trigger: 'blur' }],
seqEnd: [{ required: true, message: '请输入结束序号', trigger: 'blur' }],
digitCount: [{ required: true, message: '请输入序号位数', trigger: 'blur' }],
digitCount: [{ required: true, message: '请输入序号位数', trigger: 'blur' }]
}
const typePrefixMap: Record<string, string> = { zxbm: 'ZKZX', qdbm: 'ZKQD' }
const batchGenPreview = computed(() => {
@@ -535,10 +551,10 @@
const batchIssueLoading = ref(false)
const batchIssueFormRef = ref()
const batchIssueForm = reactive({
issuedUserId: '',
issuedUserId: ''
})
const batchIssueRules = {
issuedUserId: [{ required: true, message: '请选择下发用户', trigger: 'change' }],
issuedUserId: [{ required: true, message: '请选择下发用户', trigger: 'change' }]
}
const userOptions = ref<Array<{ label: string; value: string }>>([])
@@ -746,7 +762,7 @@
middle: batchGenForm.middle.trim(),
seqStart: batchGenForm.seqStart,
seqEnd: batchGenForm.seqEnd,
digitCount: batchGenForm.digitCount,
digitCount: batchGenForm.digitCount
})
batchGenVisible.value = false
await loadData()
@@ -767,12 +783,12 @@
await batchIssueFormRef.value?.validate?.()
batchIssueLoading.value = true
try {
const ids = selectedRows.value.map(r => r.id)
const userOpt = userOptions.value.find(u => u.value === batchIssueForm.issuedUserId)
const ids = selectedRows.value.map((r) => r.id)
const userOpt = userOptions.value.find((u) => u.value === batchIssueForm.issuedUserId)
await fetchBatchIssueSampleCodes({
ids,
issuedUserId: batchIssueForm.issuedUserId,
issuedUserName: userOpt?.label || undefined,
issuedUserName: userOpt?.label || undefined
})
batchIssueVisible.value = false
selectedRows.value = []
@@ -802,6 +818,5 @@
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
@@ -50,7 +50,9 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按协议号、姓名模糊检索;待办状态默认未办保险,满月日期默认近 30 天。</span>
<span class="filter-hint"
>支持按协议号、姓名模糊检索;待办状态默认未办保险,满月日期默认近 30 天。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -123,9 +125,11 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{
protocolStatusLabel(String(row.protocolStatus || ''))
}}</el-tag>
<el-tag
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
size="small"
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column
@@ -297,11 +301,7 @@
<el-input v-model="formData.insurancePolicyNo" placeholder="请输入保险单号" />
</el-form-item>
<el-form-item label="保单费用(元)">
<AmountInput
v-model="formData.insuranceFee"
:min="0"
placeholder="请输入保单费用"
/>
<AmountInput v-model="formData.insuranceFee" :min="0" placeholder="请输入保单费用" />
</el-form-item>
<el-form-item label="保险单生效时间">
<el-date-picker
@@ -413,9 +413,7 @@
</div>
</div>
</div>
<span class="upload-hint"
>支持图片、视频、PDF、Word、Excel等文件</span
>
<span class="upload-hint">支持图片、视频、PDF、Word、Excel等文件</span>
</div>
</el-form-item>
<el-form-item label="办理备注" class="form-col-span-2">
@@ -486,10 +484,7 @@
fetchInsuranceCompanyList,
fetchInsuranceProjectPrices
} from '@/api/admin'
import {
protocolStatusLabel,
protocolStatusTagType,
} from '@/utils/status-tag-registry'
import { protocolStatusLabel, protocolStatusTagType } from '@/utils/status-tag-registry'
defineOptions({ name: 'CellStorageFullmoonTodo' })
@@ -39,8 +39,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按物料名称/编码查询,可按变动类型和日期范围筛选。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -107,12 +111,12 @@
/>
<el-table-column label="领用人" min-width="100" align="center" header-align="center">
<template #default="{ row }">
{{ row.changeType === 'ck' ? (row.receiver || '-') : '-' }}
{{ row.changeType === 'ck' ? row.receiver || '-' : '-' }}
</template>
</el-table-column>
<el-table-column label="用途" min-width="120" align="center" header-align="center">
<template #default="{ row }">
{{ row.changeType === 'ck' ? (row.purpose || '-') : '-' }}
{{ row.changeType === 'ck' ? row.purpose || '-' : '-' }}
</template>
</el-table-column>
<el-table-column label="操作人" min-width="100" align="center" header-align="center">
@@ -120,7 +124,13 @@
{{ row.createByName || row.createBy || '-' }}
</template>
</el-table-column>
<el-table-column label="备注" min-width="140" align="center" header-align="center" show-overflow-tooltip>
<el-table-column
label="备注"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.remark || '-' }}
</template>
@@ -132,7 +142,13 @@
align="center"
header-align="center"
/>
<el-table-column label="操作" min-width="160" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="160"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
@@ -208,8 +224,8 @@
{ label: '变动数量', value: formatChangeQty(row).text },
{ label: '变动前库存', value: String(row.stockBefore ?? 0) },
{ label: '变动后库存', value: String(row.stockAfter ?? 0) },
{ label: '领用人', value: row.changeType === 'ck' ? (row.receiver || '-') : '-' },
{ label: '用途', value: row.changeType === 'ck' ? (row.purpose || '-') : '-' },
{ label: '领用人', value: row.changeType === 'ck' ? row.receiver || '-' : '-' },
{ label: '用途', value: row.changeType === 'ck' ? row.purpose || '-' : '-' },
{ label: '操作人', value: row.createByName || row.createBy || '-' },
{ label: '备注', value: row.remark || '—', span: true },
{ label: '操作时间', value: row.createTime }
@@ -264,13 +280,10 @@
)
// 日期范围变更时自动搜索
watch(
dateRange,
() => {
pagination.page = 1
loadData()
}
)
watch(dateRange, () => {
pagination.page = 1
loadData()
})
onMounted(() => {
loadData()
@@ -38,8 +38,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编码、名称模糊查询,可按分类、状态筛选。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -154,15 +158,29 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" min-width="400" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="400"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleStockIn(row)"><el-icon><Upload /></el-icon>入库</el-button>
<el-button class="btn-op-item" @click="handleStockOut(row)"><el-icon><Download /></el-icon>出库</el-button>
<el-button class="btn-op-item" @click="handleStockIn(row)"
><el-icon><Upload /></el-icon>入库</el-button
>
<el-button class="btn-op-item" @click="handleStockOut(row)"
><el-icon><Download /></el-icon>出库</el-button
>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="handleEdit(row)"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)"><el-icon><Delete /></el-icon>删除</el-button>
<el-button class="btn-op-item primary" @click="handleEdit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDelete(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -238,7 +256,9 @@
</el-select>
</el-form-item>
<el-form-item label="预警库存">
<template #label>预警库存<span class="form-label-tip">低于该值时列表显示库存预警</span></template>
<template #label
>预警库存<span class="form-label-tip">低于该值时列表显示库存预警</span></template
>
<el-input-number
v-model="formData.warningQty"
:min="0"
@@ -255,12 +275,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="可选"
/>
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
</div>
</el-form>
@@ -314,12 +329,7 @@
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="stockInForm.remark"
type="textarea"
:rows="3"
placeholder="可选"
/>
<el-input v-model="stockInForm.remark" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
@@ -397,20 +407,10 @@
</el-select>
</el-form-item>
<el-form-item label="用途">
<el-input
v-model="stockOutForm.purpose"
type="textarea"
:rows="2"
placeholder="可选"
/>
<el-input v-model="stockOutForm.purpose" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="stockOutForm.remark"
type="textarea"
:rows="2"
placeholder="可选"
/>
<el-input v-model="stockOutForm.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
@@ -511,13 +511,41 @@
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column prop="materialCode" label="物料编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="materialName" label="物料名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column label="当前库存" min-width="100" align="center" header-align="center">
<el-table-column
prop="materialCode"
label="物料编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="物料名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
label="当前库存"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stockQty }}</template>
</el-table-column>
<el-table-column label="入库数量" min-width="130" align="center" header-align="center">
<el-table-column
label="入库数量"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input-number
v-model="row.qty"
@@ -531,22 +559,28 @@
</el-table-column>
<el-table-column label="备注" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="可选" clearable class="inner-cell-input" />
<el-input
v-model="row.remark"
placeholder="可选"
clearable
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="batchStockInLines.splice($index, 1)">
<el-button
link
type="danger"
size="small"
@click="batchStockInLines.splice($index, 1)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="请通过上方搜索框添加要入库的物料"
:image-size="56"
/>
<el-empty v-else description="请通过上方搜索框添加要入库的物料" :image-size="56" />
</div>
</el-form>
<template #footer>
@@ -675,13 +709,41 @@
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column prop="materialCode" label="物料编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="materialName" label="物料名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column label="当前库存" min-width="100" align="center" header-align="center">
<el-table-column
prop="materialCode"
label="物料编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="物料名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
label="当前库存"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stockQty }}</template>
</el-table-column>
<el-table-column label="出库数量" min-width="130" align="center" header-align="center">
<el-table-column
label="出库数量"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input-number
v-model="row.qty"
@@ -696,22 +758,28 @@
</el-table-column>
<el-table-column label="备注" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="可选" clearable class="inner-cell-input" />
<el-input
v-model="row.remark"
placeholder="可选"
clearable
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="batchStockOutLines.splice($index, 1)">
<el-button
link
type="danger"
size="small"
@click="batchStockOutLines.splice($index, 1)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="请通过上方搜索框添加要出库的物料"
:image-size="56"
/>
<el-empty v-else description="请通过上方搜索框添加要出库的物料" :image-size="56" />
</div>
</el-form>
<template #footer>
@@ -746,7 +814,19 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Box, RefreshLeft, Search, Upload, Download, Edit, Delete } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
Box,
RefreshLeft,
Search,
Upload,
Download,
Edit,
Delete
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { fetchDictDataOptions, fetchItemUnitList, fetchUserList } from '@/api/admin'
import {
@@ -989,7 +1069,13 @@
{
label: '物料分类',
value: getCategoryLabel(row.category),
tagType: getCategoryTagType(row.category) as 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
tagType: getCategoryTagType(row.category) as
| 'success'
| 'warning'
| 'danger'
| 'info'
| ''
| 'primary'
},
{ label: '规格型号', value: row.specModel },
{ label: '单位', value: row.unit },
@@ -55,7 +55,8 @@
</div>
<div class="filter-actions-row">
<span class="filter-hint"
>支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示近 30 天内应缴费的明细。</span
>支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示近 30
天内应缴费的明细。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
@@ -70,7 +71,14 @@
</div>
<div class="list-body list-wrap payment-confirm-table-wrap">
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id" @row-dblclick="handleRowDblClick">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@row-dblclick="handleRowDblClick"
>
<el-table-column
type="index"
label="序号"
@@ -79,14 +87,38 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="protocolNo" label="协议号" min-width="130" align="center" header-align="center" />
<el-table-column prop="motherName" label="客户姓名" min-width="100" align="center" header-align="center">
<el-table-column
prop="protocolNo"
label="协议号"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="motherName"
label="客户姓名"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.motherName || '-' }}</template>
</el-table-column>
<el-table-column prop="motherIdCard" label="身份证号" min-width="170" align="center" header-align="center">
<el-table-column
prop="motherIdCard"
label="身份证号"
min-width="170"
align="center"
header-align="center"
>
<template #default="{ row }">{{ maskIdCard(row.motherIdCard) }}</template>
</el-table-column>
<el-table-column prop="newbornName" label="新生儿姓名" min-width="110" align="center" header-align="center">
<el-table-column
prop="newbornName"
label="新生儿姓名"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.newbornName || '-' }}</template>
</el-table-column>
<el-table-column
@@ -98,33 +130,95 @@
>
<template #default="{ row }">{{ maskIdCard(row.newbornIdCard) }}</template>
</el-table-column>
<el-table-column prop="protocolStatus" label="协议状态" min-width="100" align="center" header-align="center">
<el-table-column
prop="protocolStatus"
label="协议状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag>
<el-tag
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
size="small"
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column prop="payMode" label="缴费方式" min-width="100" align="center" header-align="center">
<el-table-column
prop="payMode"
label="缴费方式"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ payModeLabel(String(row.payMode || '')) }}</template>
</el-table-column>
<el-table-column prop="installmentNo" label="期数" min-width="80" align="center" header-align="center">
<el-table-column
prop="installmentNo"
label="期数"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.installmentNo ?? '-' }}</template>
</el-table-column>
<el-table-column prop="installmentDueDate" label="应缴费时间" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ formatPaymentDueDate(row.installmentDueDate) }}</template>
<el-table-column
prop="installmentDueDate"
label="应缴费时间"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{
formatPaymentDueDate(row.installmentDueDate)
}}</template>
</el-table-column>
<el-table-column prop="dueAmount" label="应缴金额(元)" min-width="120" align="center" header-align="center">
<el-table-column
prop="dueAmount"
label="应缴金额(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ formatPaymentAmount(row.dueAmount) }}</template>
</el-table-column>
<el-table-column prop="transferAmount" label="实缴金额(元)" min-width="120" align="center" header-align="center">
<el-table-column
prop="transferAmount"
label="实缴金额(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ formatPaymentAmount(row.transferAmount) }}</template>
</el-table-column>
<el-table-column prop="transferType" label="转账方式" min-width="110" align="center" header-align="center">
<template #default="{ row }">{{ transferTypeLabel(String(row.transferType || '')) }}</template>
<el-table-column
prop="transferType"
label="转账方式"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">{{
transferTypeLabel(String(row.transferType || ''))
}}</template>
</el-table-column>
<el-table-column prop="transferTime" label="转账时间" min-width="170" align="center" header-align="center">
<el-table-column
prop="transferTime"
label="转账时间"
min-width="170"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.transferTime || '-' }}</template>
</el-table-column>
<el-table-column prop="payStatus" label="状态" min-width="110" align="center" header-align="center">
<el-table-column
prop="payStatus"
label="状态"
min-width="110"
align="center"
header-align="center"
>
<template #default>
<el-tag :type="activeTab === 'pending' ? 'warning' : 'success'" size="small">
{{ activeTab === 'pending' ? '缴费确认中' : '已缴费' }}
@@ -158,7 +252,13 @@
align="center"
header-align="center"
/>
<el-table-column label="操作" min-width="140" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="140"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button
v-if="activeTab === 'pending'"
@@ -167,7 +267,9 @@
>
确认收款
</el-button>
<el-button v-else class="btn-op-item" @click="openViewConfirm(row)">查看确认信息</el-button>
<el-button v-else class="btn-op-item" @click="openViewConfirm(row)"
>查看确认信息</el-button
>
</template>
</el-table-column>
</el-table>
@@ -213,7 +315,12 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="submitConfirm">
<el-button
class="btn-main"
type="primary"
:loading="submitLoading"
@click="submitConfirm"
>
<el-icon><Check /></el-icon>
确认
</el-button>
@@ -4,7 +4,9 @@
<div class="page-container">
<header class="page-head">
<h1>缴费信息</h1>
<p class="page-desc">查看采集协议单缴费明细,按状态分为待缴费、缴费确认中、已缴费、退费确认中、已退费。</p>
<p class="page-desc"
>查看采集协议单缴费明细,按状态分为待缴费、缴费确认中、已缴费、退费确认中、已退费。</p
>
</header>
<div class="region-tabs region-tab-strip">
@@ -74,10 +76,7 @@
</el-form>
</div>
<div
v-if="activeTab === 'dfj' || activeTab === 'yjf'"
class="list-toolbar"
>
<div v-if="activeTab === 'dfj' || activeTab === 'yjf'" class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
@@ -178,7 +177,11 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag>
<el-tag
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
size="small"
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column
@@ -379,8 +382,14 @@
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
<el-icon class="action-btn" title="删除" @click="removeRefundAttachment(fIdx)">
<span class="attachment-name" :title="file.fileName">{{
file.fileName
}}</span>
<el-icon
class="action-btn"
title="删除"
@click="removeRefundAttachment(fIdx)"
>
<Delete />
</el-icon>
</div>
@@ -580,11 +589,15 @@
const id = String(row.id || '')
if (!id) return
try {
await ElMessageBox.confirm('确认对该缴费明细进行缴费确认?提交后状态变为「缴费确认中」。', '缴费确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
await ElMessageBox.confirm(
'确认对该缴费明细进行缴费确认?提交后状态变为「缴费确认中」。',
'缴费确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
await fetchSubmitReceiptFinance(id)
loadData()
} catch {
@@ -625,21 +638,12 @@
refundFormRef.value?.clearValidate()
}
function openRefundFinanceDialog(
row?: Record<string, unknown>,
batchIds?: string[]
) {
const ids = batchIds?.length
? batchIds
: row?.id
? [String(row.id)]
: []
function openRefundFinanceDialog(row?: Record<string, unknown>, batchIds?: string[]) {
const ids = batchIds?.length ? batchIds : row?.id ? [String(row.id)] : []
if (!ids.length) return
refundTargetIds.value = ids
const defaultAmount =
!batchIds?.length && row?.transferAmount != null
? Number(row.transferAmount)
: undefined
!batchIds?.length && row?.transferAmount != null ? Number(row.transferAmount) : undefined
resetRefundForm(
defaultAmount != null && !Number.isNaN(defaultAmount) ? defaultAmount : undefined
)
@@ -16,7 +16,11 @@
<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-input
v-model="searchForm.keyword"
placeholder="协议号/姓名/身份证号/电话"
clearable
/>
</el-form-item>
<el-form-item label="协议状态">
<el-select
@@ -57,7 +61,9 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span>
<span class="filter-hint"
>列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -83,16 +89,32 @@
<el-icon><CircleCheck /></el-icon>
审核
</el-button>
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="openStatusChangeDialog('activate')">
<el-button
class="btn-ghost"
:disabled="!selectedRows.length"
@click="openStatusChangeDialog('activate')"
>
协议生效
</el-button>
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="openStatusChangeDialog('expire')">
<el-button
class="btn-ghost"
:disabled="!selectedRows.length"
@click="openStatusChangeDialog('expire')"
>
协议过期
</el-button>
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="openStatusChangeDialog('void')">
<el-button
class="btn-ghost"
:disabled="!selectedRows.length"
@click="openStatusChangeDialog('void')"
>
协议作废
</el-button>
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="openStatusChangeDialog('toResearch')">
<el-button
class="btn-ghost"
:disabled="!selectedRows.length"
@click="openStatusChangeDialog('toResearch')"
>
协议转科研
</el-button>
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="handleBatchDelete">
@@ -136,9 +158,11 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{
protocolStatusLabel(String(row.protocolStatus || ''))
}}</el-tag>
<el-tag
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
size="small"
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column
@@ -345,7 +369,12 @@
fetchDeleteCollectionProtocol,
type CollectionProtocolRow
} from '@/api/collection-protocol'
import { protocolStatusLabel, protocolStatusTagType, newbornSexLabel, deliveryModeLabel } from '@/utils/status-tag-registry'
import {
protocolStatusLabel,
protocolStatusTagType,
newbornSexLabel,
deliveryModeLabel
} from '@/utils/status-tag-registry'
defineOptions({ name: 'CellStorageProtocolAudit' })
File diff suppressed because it is too large. Load diff
@@ -787,9 +787,7 @@
<div class="pdc-info-row">
<div class="pdc-info-item">
<span class="info-label">应缴费金额(元)</span>
<span class="info-value">{{
fmtMoney(detail.dueAmount)
}}</span>
<span class="info-value">{{ fmtMoney(detail.dueAmount) }}</span>
</div>
<div class="pdc-info-item">
<span class="info-label">转账方式</span>
@@ -799,9 +797,7 @@
</div>
<div class="pdc-info-item">
<span class="info-label">转账金额(元)</span>
<span class="info-value">{{
fmtMoney(detail.transferAmount)
}}</span>
<span class="info-value">{{ fmtMoney(detail.transferAmount) }}</span>
</div>
<div class="pdc-info-item">
<span class="info-label">转账时间</span>
@@ -16,7 +16,11 @@
<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-input
v-model="searchForm.keyword"
placeholder="协议号/姓名/身份证号/电话"
clearable
/>
</el-form-item>
<el-form-item label="协议状态">
<el-select
@@ -57,7 +61,9 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span>
<span class="filter-hint"
>列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -120,9 +126,11 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{
protocolStatusLabel(String(row.protocolStatus || ''))
}}</el-tag>
<el-tag
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
size="small"
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column
@@ -287,14 +295,7 @@
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus,
View,
Edit,
Delete,
Search,
RefreshLeft
} from '@element-plus/icons-vue'
import { Plus, View, Edit, Delete, Search, RefreshLeft } from '@element-plus/icons-vue'
import ProtocolFormDialog from './ProtocolFormDialog.vue'
import ProtocolViewDialog from './ProtocolViewDialog.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
@@ -303,7 +304,12 @@
fetchDeleteCollectionProtocol,
type CollectionProtocolRow
} from '@/api/collection-protocol'
import { protocolStatusLabel, protocolStatusTagType, newbornSexLabel, deliveryModeLabel } from '@/utils/status-tag-registry'
import {
protocolStatusLabel,
protocolStatusTagType,
newbornSexLabel,
deliveryModeLabel
} from '@/utils/status-tag-registry'
defineOptions({ name: 'CellStorageProtocol' })
@@ -55,7 +55,8 @@
</div>
<div class="filter-actions-row">
<span class="filter-hint"
>支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示近 30 天内应缴费的明细。</span
>支持协议号、客户姓名、身份证号、新生儿姓名、新生儿身份证号模糊检索;默认展示近 30
天内应缴费的明细。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
@@ -70,7 +71,14 @@
</div>
<div class="list-body list-wrap refund-confirm-table-wrap">
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="id" @row-dblclick="handleRowDblClick">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@row-dblclick="handleRowDblClick"
>
<el-table-column
type="index"
label="序号"
@@ -79,14 +87,38 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="protocolNo" label="协议号" min-width="130" align="center" header-align="center" />
<el-table-column prop="motherName" label="客户姓名" min-width="100" align="center" header-align="center">
<el-table-column
prop="protocolNo"
label="协议号"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="motherName"
label="客户姓名"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.motherName || '-' }}</template>
</el-table-column>
<el-table-column prop="motherIdCard" label="身份证号" min-width="170" align="center" header-align="center">
<el-table-column
prop="motherIdCard"
label="身份证号"
min-width="170"
align="center"
header-align="center"
>
<template #default="{ row }">{{ maskIdCard(row.motherIdCard) }}</template>
</el-table-column>
<el-table-column prop="newbornName" label="新生儿姓名" min-width="110" align="center" header-align="center">
<el-table-column
prop="newbornName"
label="新生儿姓名"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.newbornName || '-' }}</template>
</el-table-column>
<el-table-column
@@ -98,33 +130,95 @@
>
<template #default="{ row }">{{ maskIdCard(row.newbornIdCard) }}</template>
</el-table-column>
<el-table-column prop="protocolStatus" label="协议状态" min-width="100" align="center" header-align="center">
<el-table-column
prop="protocolStatus"
label="协议状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag>
<el-tag
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
size="small"
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column prop="payMode" label="缴费方式" min-width="100" align="center" header-align="center">
<el-table-column
prop="payMode"
label="缴费方式"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ payModeLabel(String(row.payMode || '')) }}</template>
</el-table-column>
<el-table-column prop="installmentNo" label="期数" min-width="80" align="center" header-align="center">
<el-table-column
prop="installmentNo"
label="期数"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.installmentNo ?? '-' }}</template>
</el-table-column>
<el-table-column prop="installmentDueDate" label="应缴费时间" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ formatPaymentDueDate(row.installmentDueDate) }}</template>
<el-table-column
prop="installmentDueDate"
label="应缴费时间"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{
formatPaymentDueDate(row.installmentDueDate)
}}</template>
</el-table-column>
<el-table-column prop="dueAmount" label="应缴金额(元)" min-width="120" align="center" header-align="center">
<el-table-column
prop="dueAmount"
label="应缴金额(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ formatPaymentAmount(row.dueAmount) }}</template>
</el-table-column>
<el-table-column prop="transferAmount" label="实缴金额(元)" min-width="120" align="center" header-align="center">
<el-table-column
prop="transferAmount"
label="实缴金额(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ formatPaymentAmount(row.transferAmount) }}</template>
</el-table-column>
<el-table-column prop="transferType" label="转账方式" min-width="110" align="center" header-align="center">
<template #default="{ row }">{{ transferTypeLabel(String(row.transferType || '')) }}</template>
<el-table-column
prop="transferType"
label="转账方式"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">{{
transferTypeLabel(String(row.transferType || ''))
}}</template>
</el-table-column>
<el-table-column prop="transferTime" label="转账时间" min-width="170" align="center" header-align="center">
<el-table-column
prop="transferTime"
label="转账时间"
min-width="170"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.transferTime || '-' }}</template>
</el-table-column>
<el-table-column prop="refundAmount" label="退费金额(元)" min-width="120" align="center" header-align="center">
<el-table-column
prop="refundAmount"
label="退费金额(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ formatPaymentAmount(row.refundAmount) }}</template>
</el-table-column>
<el-table-column
@@ -137,7 +231,13 @@
>
<template #default="{ row }">{{ row.refundRemark || '-' }}</template>
</el-table-column>
<el-table-column prop="payStatus" label="状态" min-width="110" align="center" header-align="center">
<el-table-column
prop="payStatus"
label="状态"
min-width="110"
align="center"
header-align="center"
>
<template #default>
<el-tag :type="activeTab === 'pending' ? 'warning' : 'success'" size="small">
{{ activeTab === 'pending' ? '退费确认中' : '已退费' }}
@@ -171,7 +271,13 @@
align="center"
header-align="center"
/>
<el-table-column label="操作" min-width="140" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="140"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button
v-if="activeTab === 'pending'"
@@ -180,7 +286,9 @@
>
确认退费
</el-button>
<el-button v-else class="btn-op-item" @click="openViewConfirm(row)">查看确认信息</el-button>
<el-button v-else class="btn-op-item" @click="openViewConfirm(row)"
>查看确认信息</el-button
>
</template>
</el-table-column>
</el-table>
@@ -228,7 +336,12 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="submitConfirm">
<el-button
class="btn-main"
type="primary"
:loading="submitLoading"
@click="submitConfirm"
>
<el-icon><Check /></el-icon>
确认
</el-button>
@@ -460,7 +573,8 @@
submitLoading.value = true
try {
await fetchRefundConfirmSubmit(currentDetailId.value, {
refundFinanceConfirmAttachments: financeAttachmentsToUrlString(attachments.value) || undefined,
refundFinanceConfirmAttachments:
financeAttachmentsToUrlString(attachments.value) || undefined,
refundFinanceRemark: financeRemark.value.trim() || undefined
})
dialogVisible.value = false
@@ -18,14 +18,24 @@
<el-input v-model="searchForm.keyword" placeholder="协议号/姓名/样本编码" clearable />
</el-form-item>
<el-form-item label="接收结论">
<el-select v-model="searchForm.acceptConclusion" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.acceptConclusion"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="接收" value="js" />
<el-option label="拒收" value="bj" />
<el-option label="待接收确认" value="dqr" />
</el-select>
</el-form-item>
<el-form-item label="通知状态">
<el-select v-model="searchForm.smsNotified" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.smsNotified"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="已通知" value="1" />
<el-option label="未通知" value="0" />
</el-select>
@@ -43,7 +53,9 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按协议号、姓名、样本编码模糊检索;默认展示近 30 天内接收的样本。</span>
<span class="filter-hint"
>支持按协议号、姓名、样本编码模糊检索;默认展示近 30 天内接收的样本。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -73,7 +85,13 @@
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="protocolNo" label="协议号" min-width="140" align="center" header-align="center" />
<el-table-column
prop="protocolNo"
label="协议号"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="protocolStatus"
label="协议状态"
@@ -82,34 +100,98 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{
protocolStatusLabel(String(row.protocolStatus || ''))
}}</el-tag>
<el-tag
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
size="small"
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column prop="motherName" label="母亲姓名" min-width="100" align="center" header-align="center" />
<el-table-column prop="newbornName" label="新生儿姓名" min-width="110" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="sampleCode" label="样本编码" min-width="130" align="center" header-align="center" />
<el-table-column prop="sampleTypeName" label="样本类型" min-width="110" align="center" header-align="center" />
<el-table-column prop="receptionTime" label="接收时间" min-width="170" align="center" header-align="center" />
<el-table-column prop="acceptConclusion" label="接收结论" min-width="110" align="center" header-align="center">
<el-table-column
prop="motherName"
label="母亲姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="newbornName"
label="新生儿姓名"
min-width="110"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="sampleCode"
label="样本编码"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="sampleTypeName"
label="样本类型"
min-width="110"
align="center"
header-align="center"
/>
<el-table-column
prop="receptionTime"
label="接收时间"
min-width="170"
align="center"
header-align="center"
/>
<el-table-column
prop="acceptConclusion"
label="接收结论"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
:type="row.acceptConclusion === 'js' ? 'success' : row.acceptConclusion === 'bj' ? 'danger' : 'warning'"
:type="
row.acceptConclusion === 'js'
? 'success'
: row.acceptConclusion === 'bj'
? 'danger'
: 'warning'
"
size="small"
>
{{ row.acceptConclusionLabel || (row.acceptConclusion === 'js' ? '接收' : row.acceptConclusion === 'bj' ? '拒收' : '待接收确认') }}
{{
row.acceptConclusionLabel ||
(row.acceptConclusion === 'js'
? '接收'
: row.acceptConclusion === 'bj'
? '拒收'
: '待接收确认')
}}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="smsNotified" label="通知状态" min-width="100" align="center" header-align="center">
<el-table-column
prop="smsNotified"
label="通知状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.smsNotified == 1 ? 'success' : 'info'" size="small">
{{ row.smsNotified == 1 ? '已通知' : '未通知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="360" 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>查看
@@ -160,10 +242,7 @@
fetchGenerateSms
} from '@/api/sample-sms-notification'
import { type CollectionProtocolRow } from '@/api/collection-protocol'
import {
protocolStatusLabel,
protocolStatusTagType,
} from '@/utils/status-tag-registry'
import { protocolStatusLabel, protocolStatusTagType } from '@/utils/status-tag-registry'
defineOptions({ name: 'CellStorageSampleReceptionInfo' })
@@ -19,7 +19,12 @@
<el-input v-model="searchForm.keyword" placeholder="协议号/姓名" clearable />
</el-form-item>
<el-form-item label="制作状态">
<el-select v-model="searchForm.makeStatus" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.makeStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="未制作" value="0" />
<el-option label="已制作" value="1" />
</el-select>
@@ -37,7 +42,9 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按协议号、姓名模糊检索;制作状态默认未制作,入库日期默认近 30 天。</span>
<span class="filter-hint"
>支持按协议号、姓名模糊检索;制作状态默认未制作,入库日期默认近 30 天。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -68,7 +75,13 @@
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="protocolNo" label="采集协议号" min-width="160" align="center" header-align="center" />
<el-table-column
prop="protocolNo"
label="采集协议号"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="protocolStatus"
label="协议状态"
@@ -77,34 +90,92 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{
protocolStatusLabel(String(row.protocolStatus || ''))
}}</el-tag>
<el-tag
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
size="small"
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column prop="motherName" label="母亲姓名" min-width="100" align="center" header-align="center" />
<el-table-column prop="motherPhone" label="母亲联系方式" min-width="130" align="center" header-align="center">
<el-table-column
prop="motherName"
label="母亲姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="motherPhone"
label="母亲联系方式"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.motherPhone || '—' }}</template>
</el-table-column>
<el-table-column prop="motherIdCard" label="身份证号" min-width="170" align="center" header-align="center">
<el-table-column
prop="motherIdCard"
label="身份证号"
min-width="170"
align="center"
header-align="center"
>
<template #default="{ row }">{{ maskIdCard(row.motherIdCard) }}</template>
</el-table-column>
<el-table-column prop="newbornInfo" label="新生儿信息" min-width="200" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="sampleInfo" label="样本信息" min-width="200" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="storageInDate" label="入库日期" min-width="120" align="center" header-align="center">
<el-table-column
prop="newbornInfo"
label="新生儿信息"
min-width="200"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="sampleInfo"
label="样本信息"
min-width="200"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="storageInDate"
label="入库日期"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ formatStorageInDate(row.storageInDate) }}</template>
</el-table-column>
<el-table-column prop="makeStatus" label="制作状态" min-width="100" align="center" header-align="center">
<el-table-column
prop="makeStatus"
label="制作状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.makeStatus == '1' ? 'success' : 'warning'" size="small">
{{ row.makeStatus == '1' ? '已制作' : '未制作' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="makeDate" label="制作日期" min-width="120" align="center" header-align="center">
<el-table-column
prop="makeDate"
label="制作日期"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ formatStorageInDate(row.makeDate) }}</template>
</el-table-column>
<el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleUpload(row)">
<el-icon><Upload /></el-icon>上传储存证
@@ -180,9 +251,7 @@
<div
class="attachment-preview"
@click="
isImageFile(file) && file.fileUrl
? handlePreviewAttachment(file)
: undefined
isImageFile(file) && file.fileUrl ? handlePreviewAttachment(file) : undefined
"
>
<template v-if="isImageFile(file) && file.fileUrl">
@@ -244,7 +313,12 @@
<el-button class="btn-ghost" @click="uploadDialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmitUpload">
<el-button
class="btn-main"
type="primary"
:loading="submitLoading"
@click="handleSubmitUpload"
>
<el-icon><Check /></el-icon>确定
</el-button>
</template>
@@ -312,10 +386,7 @@
import { fetchUploadAttachment } from '@/api/business'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { maskIdCard } from '@/utils/mask-sensitive'
import {
protocolStatusLabel,
protocolStatusTagType,
} from '@/utils/status-tag-registry'
import { protocolStatusLabel, protocolStatusTagType } from '@/utils/status-tag-registry'
interface CertAttachmentItem {
id: string
@@ -18,14 +18,24 @@
<el-input v-model="searchForm.keyword" placeholder="协议号/姓名/样本编码" clearable />
</el-form-item>
<el-form-item label="入库状态">
<el-select v-model="searchForm.storageStatus" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.storageStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="已入库" value="yrk" />
<el-option label="拒绝入库" value="btg" />
<el-option label="待入库确认" value="drk" />
</el-select>
</el-form-item>
<el-form-item label="通知状态">
<el-select v-model="searchForm.smsNotified" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.smsNotified"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="已通知" value="1" />
<el-option label="未通知" value="0" />
</el-select>
@@ -43,7 +53,9 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持按协议号、姓名、样本编码模糊检索;默认展示近 30 天内入库的样本。</span>
<span class="filter-hint"
>支持按协议号、姓名、样本编码模糊检索;默认展示近 30 天内入库的样本。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -73,7 +85,13 @@
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="protocolNo" label="协议号" min-width="140" align="center" header-align="center" />
<el-table-column
prop="protocolNo"
label="协议号"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="protocolStatus"
label="协议状态"
@@ -82,17 +100,56 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="protocolStatusTagType(String(row.protocolStatus || ''))" size="small">{{
protocolStatusLabel(String(row.protocolStatus || ''))
}}</el-tag>
<el-tag
:type="protocolStatusTagType(String(row.protocolStatus || ''))"
size="small"
>{{ protocolStatusLabel(String(row.protocolStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column prop="motherName" label="母亲姓名" min-width="100" align="center" header-align="center" />
<el-table-column prop="newbornName" label="新生儿姓名" min-width="110" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="sampleCode" label="样本编码" min-width="130" align="center" header-align="center" />
<el-table-column prop="sampleTypeName" label="样本类型" min-width="110" align="center" header-align="center" />
<el-table-column prop="storageTime" label="入库时间" min-width="170" align="center" header-align="center" />
<el-table-column prop="storageStatusLabel" label="入库状态" min-width="100" align="center" header-align="center">
<el-table-column
prop="motherName"
label="母亲姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="newbornName"
label="新生儿姓名"
min-width="110"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="sampleCode"
label="样本编码"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="sampleTypeName"
label="样本类型"
min-width="110"
align="center"
header-align="center"
/>
<el-table-column
prop="storageTime"
label="入库时间"
min-width="170"
align="center"
header-align="center"
/>
<el-table-column
prop="storageStatusLabel"
label="入库状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
:type="
@@ -108,14 +165,26 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="smsNotified" label="通知状态" min-width="100" align="center" header-align="center">
<el-table-column
prop="smsNotified"
label="通知状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.smsNotified == 1 ? 'success' : 'info'" size="small">
{{ row.smsNotified == 1 ? '已通知' : '未通知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="360" 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>查看
@@ -166,10 +235,7 @@
fetchGenerateStorageEntrySms
} from '@/api/storage-entry-sms-notification'
import { type CollectionProtocolRow } from '@/api/collection-protocol'
import {
protocolStatusLabel,
protocolStatusTagType,
} from '@/utils/status-tag-registry'
import { protocolStatusLabel, protocolStatusTagType } from '@/utils/status-tag-registry'
defineOptions({ name: 'CellStorageStorageEntryInfo' })
@@ -11,7 +11,9 @@
<h2>快速入口</h2>
</div>
<div class="console-lite__body">
<p class="console-lite__hint">详见仓库根目录 `.cursorrules` 中「页面级 UI 模板(固定五页)」。</p>
<p class="console-lite__hint"
>详见仓库根目录 `.cursorrules` 中「页面级 UI 模板(固定五页)」。</p
>
<div class="console-lite__links">
<el-button
v-for="item in links"
+1 -1
View File
@@ -14,7 +14,7 @@
flex-direction: column;
min-width: 0;
height: 100vh;
overflow: hidden;
overflow: hidden;
#app-header {
position: sticky;
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编号、名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -68,11 +72,27 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="roomCode" label="编号" min-width="140" align="center" header-align="center" />
<el-table-column prop="roomName" label="名称" min-width="160" align="center" header-align="center" />
<el-table-column
prop="roomCode"
label="编号"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="roomName"
label="名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column label="等级" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag class="clean-room-grade-tag" :class="gradeTagClass(row.roomGrade)" effect="light">
<el-tag
class="clean-room-grade-tag"
:class="gradeTagClass(row.roomGrade)"
effect="light"
>
{{ gradeLabel(row.roomGrade) }}
</el-tag>
</template>
@@ -84,8 +104,20 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="areaSqm" label="面积(m²)" min-width="120" align="center" header-align="center" />
<el-table-column label="操作" min-width="320" fixed="right" align="center" header-align="center">
<el-table-column
prop="areaSqm"
label="面积(m²)"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -223,16 +255,68 @@
header-align="center"
:index="(index) => (logPagination.page - 1) * logPagination.pageSize + index + 1"
/>
<el-table-column prop="personName" label="人员姓名" min-width="100" align="center" header-align="center" />
<el-table-column prop="deptName" label="部门" min-width="100" align="center" header-align="center" />
<el-table-column prop="enterTime" label="进入时间" min-width="160" align="center" header-align="center" />
<el-table-column prop="purpose" label="目的" min-width="120" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="leaveTime" label="离开时间" min-width="160" align="center" header-align="center" />
<el-table-column prop="bodyTemp" label="体温(℃)" min-width="90" align="center" header-align="center">
<template #default="{ row }">{{ row.bodyTemp != null ? row.bodyTemp + '℃' : '-' }}</template>
<el-table-column
prop="personName"
label="人员姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="deptName"
label="部门"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="enterTime"
label="进入时间"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="purpose"
label="目的"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="leaveTime"
label="离开时间"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="bodyTemp"
label="体温(℃)"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.bodyTemp != null ? row.bodyTemp + '℃' : '-'
}}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="160" align="center" header-align="center" show-overflow-tooltip />
<el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
<el-table-column
prop="remark"
label="备注"
min-width="160"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleViewLog(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -253,7 +337,13 @@
:total="logPagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((logPagination.page = 1), (logPagination.pageSize = val), loadAccessLogData())"
@size-change="
(val) => (
(logPagination.page = 1),
(logPagination.pageSize = val),
loadAccessLogData()
)
"
@current-change="(val) => ((logPagination.page = val), loadAccessLogData())"
/>
</div>
@@ -302,7 +392,10 @@
</el-select>
</el-form-item>
<el-form-item label="部门" prop="deptName">
<el-input v-model="logFormData.deptName" placeholder="选择人员后自动填入,可手填修改" />
<el-input
v-model="logFormData.deptName"
placeholder="选择人员后自动填入,可手填修改"
/>
</el-form-item>
<el-form-item label="进入时间" prop="enterTime">
<el-date-picker
@@ -351,7 +444,12 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleLogSubmit" :loading="logSubmitLoading">
<el-button
class="btn-main"
type="primary"
@click="handleLogSubmit"
:loading="logSubmitLoading"
>
<el-icon><Check /></el-icon>
确定
</el-button>
@@ -372,7 +470,18 @@
</template>
<script setup lang="ts">
import { Plus, Close, Check, View, Edit, Delete, User, OfficeBuilding, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
Edit,
Delete,
User,
OfficeBuilding,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
import { cleanRoomGradeLabel } from '@/utils/status-tag-registry'
@@ -443,7 +552,11 @@
{ label: '编号', value: row.roomCode },
{ label: '名称', value: row.roomName },
{ label: '等级', value: gradeLabel(row.roomGrade) },
{ label: '状态', value: row.roomStatus === 'zc' ? '正常' : '异常', tagType: row.roomStatus === 'zc' ? 'success' : 'danger' },
{
label: '状态',
value: row.roomStatus === 'zc' ? '正常' : '异常',
tagType: row.roomStatus === 'zc' ? 'success' : 'danger'
},
{ label: '面积', value: row.areaSqm ? `${row.areaSqm} m²` : '' },
{ label: '备注', value: row.remark, span: true }
]
@@ -553,7 +666,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个洁净室吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个洁净室吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteCleanRoom(id)
@@ -27,9 +27,9 @@
</template>
<script setup lang="ts">
import { reactive } from 'vue'
import { reactive } from 'vue'
const form = reactive({
value: ''
})
const form = reactive({
value: ''
})
</script>
@@ -16,7 +16,11 @@
<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-input
v-model="searchForm.keyword"
placeholder="预约编号/来访单位/联系人"
clearable
/>
</el-form-item>
<el-form-item label="到访日期">
<el-date-picker
@@ -31,10 +35,16 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">按预约编号、来访单位、联系人模糊查询;默认展示未来 30 天内的到访预约。</span>
<span class="filter-hint"
>按预约编号、来访单位、联系人模糊查询;默认展示未来 30 天内的到访预约。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -79,13 +89,55 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="appointmentNo" label="预约编号" min-width="150" align="center" header-align="center" />
<el-table-column prop="visitOrg" label="来访单位" min-width="150" align="center" header-align="center" />
<el-table-column prop="contactName" label="联系人" min-width="120" align="center" header-align="center" />
<el-table-column prop="visitDate" label="到访日期" min-width="180" align="center" header-align="center" />
<el-table-column prop="visitorCount" label="人数" min-width="80" align="center" header-align="center" />
<el-table-column prop="createTime" label="创建时间" min-width="180" align="center" header-align="center" />
<el-table-column label="操作" min-width="320" fixed="right" align="center" header-align="center">
<el-table-column
prop="appointmentNo"
label="预约编号"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="visitOrg"
label="来访单位"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="contactName"
label="联系人"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="visitDate"
label="到访日期"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
prop="visitorCount"
label="人数"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
prop="createTime"
label="创建时间"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -128,7 +180,11 @@
>
<div class="form-grid-2">
<el-form-item label="预约编号">
<el-input v-model="formData.appointmentNo" placeholder="提交后自动生成" :disabled="true" />
<el-input
v-model="formData.appointmentNo"
placeholder="提交后自动生成"
:disabled="true"
/>
</el-form-item>
<el-form-item label="来访单位" prop="visitOrg">
<el-input v-model="formData.visitOrg" placeholder="请输入来访单位" />
@@ -146,7 +202,13 @@
/>
</el-form-item>
<el-form-item label="人数">
<el-input type="number" step="any" v-model="formData.visitorCount" :min="1" style="width: 100%" />
<el-input
type="number"
step="any"
v-model="formData.visitorCount"
:min="1"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
@@ -367,7 +429,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个预约吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个预约吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteExhibitionAppointment(id)
@@ -1,7 +1,14 @@
<!-- 库位管理(左树 + 右货架行布局) -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container storage-page" ref="pageContainerRef" :style="pageContainerStyle" v-loading="clearSlotsLoading" element-loading-text="正在清空格子..." element-loading-background="rgba(255,255,255,0.7)">
<div
class="page-container storage-page"
ref="pageContainerRef"
:style="pageContainerStyle"
v-loading="clearSlotsLoading"
element-loading-text="正在清空格子..."
element-loading-background="rgba(255,255,255,0.7)"
>
<header class="page-head">
<h1>库位管理</h1>
<p class="page-desc">维护库位设施树结构(罐→层→盒),以及叶子节点的格子管理。</p>
@@ -38,7 +45,10 @@
>
<template #default="{ data }">
<div class="storage-tree-node">
<span v-if="data.isLeaf === 1 && slotStatsMap[data.id]" class="storage-tree-node__stats">
<span
v-if="data.isLeaf === 1 && slotStatsMap[data.id]"
class="storage-tree-node__stats"
>
{{ slotStatsMap[data.id].occupied }}/{{ slotStatsMap[data.id].total }}
</span>
<span class="storage-tree-node__name">{{ data.nodeName }}</span>
@@ -82,11 +92,17 @@
}}
</h3>
<div class="storage-content-stats">
<span>总数 <strong>{{ slotStats.total }}</strong></span>
<span
>总数 <strong>{{ slotStats.total }}</strong></span
>
<span class="stats-dot">·</span>
<span>空闲 <strong>{{ slotStats.empty }}</strong></span>
<span
>空闲 <strong>{{ slotStats.empty }}</strong></span
>
<span class="stats-dot">·</span>
<span>占用 <strong>{{ slotStats.occupied }}</strong></span>
<span
>占用 <strong>{{ slotStats.occupied }}</strong></span
>
</div>
</div>
</div>
@@ -133,13 +149,24 @@
:class="{ occupied: cell.slotStatus === '占用' }"
@click="handleOpenOccupancyDialog(cell)"
>
<button class="cell-delete-btn" @click.stop="handleDeleteSlot(cell)" title="删除">×</button>
<button
class="cell-delete-btn"
@click.stop="handleDeleteSlot(cell)"
title="删除"
>×</button
>
<span class="cell-code">{{ cellIndex + 1 }}</span>
<span v-if="cell.occupiedCount > 0" class="cell-badge">{{ cell.occupiedCount }}</span>
<span v-if="cell.occupiedCount > 0" class="cell-badge">{{
cell.occupiedCount
}}</span>
</div>
</div>
</div>
<div class="shelf-add-row" title="在下方新增一行" @click="handleOpenAddRowDialog(ri)">
<div
class="shelf-add-row"
title="在下方新增一行"
@click="handleOpenAddRowDialog(ri)"
>
<span class="add-row-icon">+ 新增行</span>
</div>
</div>
@@ -199,7 +226,13 @@
<el-switch v-model="nodeFormData.isLeaf" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="nodeFormData.orderNum" :min="0" :step="1" controls-position="right" style="width: 100%" />
<el-input-number
v-model="nodeFormData.orderNum"
:min="0"
:step="1"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="nodeFormData.remark" placeholder="可选" />
@@ -291,10 +324,24 @@
>
<div class="form-grid-2">
<el-form-item label="行数">
<el-input type="number" step="any" v-model="batchForm.rows" :min="1" :max="20" style="width: 100%" />
<el-input
type="number"
step="any"
v-model="batchForm.rows"
:min="1"
:max="20"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="列数">
<el-input type="number" step="any" v-model="batchForm.cols" :min="1" :max="20" style="width: 100%" />
<el-input
type="number"
step="any"
v-model="batchForm.cols"
:min="1"
:max="20"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="编码前缀" class="form-col-span-2">
<template #label>
@@ -340,7 +387,13 @@
>
<el-form :model="addRowForm" label-position="top" class="storage-dialog-form">
<el-form-item label="列数">
<el-input type="number" step="any" v-model="addRowForm.cols" :min="1" style="width: 100%" />
<el-input
type="number"
step="any"
v-model="addRowForm.cols"
:min="1"
style="width: 100%"
/>
</el-form-item>
<div class="storage-batch-preview">
将在当前行下方新增 <strong>{{ addRowForm.cols }}</strong> 个格子
@@ -380,7 +433,12 @@
入库
</el-button>
</div>
<el-table :data="occupancyList" v-loading="occupancyLoading" size="small" max-height="360">
<el-table
:data="occupancyList"
v-loading="occupancyLoading"
size="small"
max-height="360"
>
<el-table-column type="index" label="序号" min-width="60" />
<el-table-column label="物品类型" min-width="120">
<template #default="{ row }">
@@ -393,7 +451,11 @@
<el-table-column prop="itemName" label="物品名称" min-width="140" />
<el-table-column label="来源" min-width="120">
<template #default="{ row }">
{{ { production: '生产入库', manual: '手动入库', transfer: '转移' }[row.itemSource] || row.itemSource }}
{{
{ production: '生产入库', manual: '手动入库', transfer: '转移' }[
row.itemSource
] || row.itemSource
}}
</template>
</el-table-column>
<el-table-column prop="storedAt" label="入库时间" min-width="170" />
@@ -431,14 +493,21 @@
<el-table-column prop="itemName" label="物品名称" min-width="120" />
<el-table-column label="来源" min-width="100">
<template #default="{ row }">
{{ { production: '生产入库', manual: '手动入库', transfer: '转移' }[row.itemSource] || row.itemSource }}
{{
{ production: '生产入库', manual: '手动入库', transfer: '转移' }[
row.itemSource
] || row.itemSource
}}
</template>
</el-table-column>
<el-table-column prop="storedAt" label="入库时间" min-width="170" />
<el-table-column prop="removedAt" label="出库时间" min-width="170" />
<el-table-column label="状态" min-width="100">
<template #default="{ row }">
<el-tag :type="row.occupancyStatus === 'stored' ? 'success' : 'warning'" size="small">
<el-tag
:type="row.occupancyStatus === 'stored' ? 'success' : 'warning'"
size="small"
>
{{ row.occupancyStatus === 'stored' ? '在库' : '已出库' }}
</el-tag>
</template>
@@ -482,7 +551,12 @@
</div>
</el-form>
<div class="occupancy-slot-save-bar">
<el-button type="primary" size="small" :loading="occupancySlotSaving" @click="handleOccupancySlotSave">
<el-button
type="primary"
size="small"
:loading="occupancySlotSaving"
@click="handleOccupancySlotSave"
>
保存
</el-button>
</div>
@@ -524,7 +598,9 @@
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="storeDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="storeSubmitLoading" @click="handleStoreSubmit">确认入库</el-button>
<el-button type="primary" :loading="storeSubmitLoading" @click="handleStoreSubmit"
>确认入库</el-button
>
</template>
</el-dialog>
</section>
@@ -702,9 +778,7 @@
})
/** 解析 slotCode "{prefix}{row}-{col}" 格式,提取行号和列号 */
const parseSlotCode = (
code: string
): { prefix: string; row: number; col: number } | null => {
const parseSlotCode = (code: string): { prefix: string; row: number; col: number } | null => {
const match = code.match(/^(.*?)(\d+)-(\d+)$/)
if (!match) return null
return { prefix: match[1], row: parseInt(match[2], 10), col: parseInt(match[3], 10) }
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按项目名称、编码模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -68,18 +72,66 @@
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
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" />
<el-table-column label="操作" min-width="320" fixed="right" 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
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -164,11 +216,7 @@
</el-select>
</el-form-item>
<el-form-item label="单价(元)" prop="unitPrice">
<AmountInput
v-model="formData.unitPrice"
:min="0"
placeholder="请输入单价"
/>
<AmountInput v-model="formData.unitPrice" :min="0" placeholder="请输入单价" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
@@ -202,7 +250,17 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Edit, Delete, ScaleToOriginal, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
Edit,
Delete,
ScaleToOriginal,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import AmountInput from '@/components/common/amount-input/index.vue'
import { formatMoneyDisplay } from '@/utils/money-format'
@@ -278,7 +336,10 @@
{ label: '项目编码', value: row.projectCode },
{ label: '细胞类型名称', value: row.cellTypeName },
{ label: '规格', value: row.specText },
{ label: '单价(元)', value: row.unitPrice != null ? `${formatMoneyDisplay(row.unitPrice)} 元` : '-' },
{
label: '单价(元)',
value: row.unitPrice != null ? `${formatMoneyDisplay(row.unitPrice)} 元` : '-'
},
{ label: '备注', value: row.remark, span: true }
]
}
@@ -441,7 +502,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条记录吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条记录吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteCellProductPrice(id)
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编码、名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -68,11 +72,41 @@
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 label="操作" min-width="320" fixed="right" align="center" header-align="center">
<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
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -156,7 +190,17 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Edit, Delete, ScaleToOriginal, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
Edit,
Delete,
ScaleToOriginal,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
@@ -309,7 +353,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个规格吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个规格吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteCellProductSpec(id)
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编码、名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -68,11 +72,41 @@
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" />
<el-table-column label="操作" min-width="320" fixed="right" align="center" header-align="center">
<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"
/>
<el-table-column
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -156,7 +190,17 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Edit, Delete, ScaleToOriginal, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
Edit,
Delete,
ScaleToOriginal,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
@@ -309,7 +353,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个类型吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个类型吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteCellProductType(id)
@@ -39,6 +39,7 @@
<el-option label="退款" value="refund" />
<el-option label="购买产品" value="purchase" />
<el-option label="退订产品" value="unsubscribe" />
<el-option label="输注结算" value="settlement" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
@@ -81,7 +82,7 @@
min-width="68"
align="center"
header-align="center"
:index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="txnType" label="流水类型" min-width="120" align="center">
<template #default="{ row }">
@@ -102,8 +103,18 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="orderName" label="订单名称" min-width="160" show-overflow-tooltip />
<el-table-column prop="channelName" label="渠道名称" min-width="160" show-overflow-tooltip />
<el-table-column
prop="orderName"
label="订单名称"
min-width="160"
show-overflow-tooltip
/>
<el-table-column
prop="channelName"
label="渠道名称"
min-width="160"
show-overflow-tooltip
/>
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
<template v-if="activeTab === 'done'">
@@ -130,7 +141,13 @@
</template>
</el-table-column>
</template>
<el-table-column label="操作" min-width="300" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="300"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
@@ -150,10 +167,7 @@
@confirm="handleVoidTxn(row)"
>
<template #reference>
<el-button
class="btn-op-item op-warn"
:loading="voidTxnId === row.id"
>
<el-button class="btn-op-item op-warn" :loading="voidTxnId === row.id">
<el-icon><Delete /></el-icon>作废
</el-button>
</template>
@@ -199,14 +213,14 @@
width="960px"
>
<template v-if="viewData.products?.length" #txn-products>
<el-table
:data="viewData.products"
stripe
class="list-table inner-table"
size="small"
>
<el-table :data="viewData.products" stripe class="list-table inner-table" size="small">
<el-table-column type="index" label="序号" min-width="60" align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="150" show-overflow-tooltip />
<el-table-column
prop="projectName"
label="项目名称"
min-width="150"
show-overflow-tooltip
/>
<el-table-column prop="cellType" label="细胞类型" min-width="120" show-overflow-tooltip />
<el-table-column prop="specText" label="规格" min-width="150" show-overflow-tooltip />
<el-table-column label="单价(元)" min-width="120" align="right">
@@ -225,10 +239,7 @@
@download="handleDownloadAttachment"
/>
</template>
<template
v-if="viewData.financeConfirmAttachmentFiles?.length"
#finance-confirm-attachments
>
<template v-if="viewData.financeConfirmAttachmentFiles?.length" #finance-confirm-attachments>
<ProtocolAttachmentReadonly
:files="viewData.financeConfirmAttachmentFiles"
@preview="handlePreviewAttachment"
@@ -261,7 +272,9 @@
</div>
<div class="confirm-flow-kv">
<span class="confirm-flow-kv__label">{{ txnAmountLabel(currentRow.txnType) }}</span>
<span class="confirm-flow-kv__value amount-text">¥{{ formatAmount(currentRow.amount) }}</span>
<span class="confirm-flow-kv__value amount-text"
>¥{{ formatAmount(currentRow.amount) }}</span
>
</div>
<div v-if="currentRow.orderName" class="confirm-flow-kv">
<span class="confirm-flow-kv__label">订单名称</span>
@@ -311,15 +324,20 @@
class="detail-section confirm-product-section"
>
<h4 class="detail-section-title">产品明细</h4>
<el-table
:data="confirmProducts"
stripe
class="list-table inner-table"
size="small"
>
<el-table :data="confirmProducts" stripe class="list-table inner-table" size="small">
<el-table-column type="index" label="序号" min-width="60" align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="150" show-overflow-tooltip />
<el-table-column prop="cellType" label="细胞类型" min-width="120" show-overflow-tooltip />
<el-table-column
prop="projectName"
label="项目名称"
min-width="150"
show-overflow-tooltip
/>
<el-table-column
prop="cellType"
label="细胞类型"
min-width="120"
show-overflow-tooltip
/>
<el-table-column prop="specText" label="规格" min-width="150" show-overflow-tooltip />
<el-table-column label="单价(元)" min-width="120" align="right">
<template #default="{ row }">¥{{ formatAmount(row.agreementUnitPrice) }}</template>
@@ -337,7 +355,12 @@
<h4 class="detail-section-title">确认信息</h4>
<el-form label-position="top" class="view-confirm-form">
<el-form-item label="财务备注">
<el-input v-model="confirmForm.remark" type="textarea" :rows="3" placeholder="请输入备注" />
<el-input
v-model="confirmForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
<el-form-item label="确认凭证" class="form-col-span-2">
<div class="attachment-upload">
@@ -394,7 +417,10 @@
@click="handlePreviewAttachment(file)"
><View
/></el-icon>
<el-icon class="action-btn" title="下载" @click="handleDownloadAttachment(file)"
<el-icon
class="action-btn"
title="下载"
@click="handleDownloadAttachment(file)"
><Download
/></el-icon>
<el-icon
@@ -454,14 +480,25 @@
<el-button class="btn-ghost" @click="amountConfirmVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="executeSubmitConfirm">
<el-button
class="btn-main"
type="primary"
:loading="submitLoading"
@click="executeSubmitConfirm"
>
<el-icon><Check /></el-icon>确认
</el-button>
</template>
</el-dialog>
<!-- 附件预览弹窗 -->
<el-dialog v-model="previewVisible" title="文件预览" width="800px" align-center destroy-on-close>
<el-dialog
v-model="previewVisible"
title="文件预览"
width="800px"
align-center
destroy-on-close
>
<div v-if="previewType === 'image'" class="preview-container">
<img
:src="previewUrl"
@@ -531,12 +568,13 @@
if (txnType === 'refund') return '退款金额(元)'
if (txnType === 'purchase') return '购买金额(元)'
if (txnType === 'unsubscribe') return '退订金额(元)'
if (txnType === 'settlement') return '结算金额(元)'
return '金额(元)'
}
/** 购买/退订流水需展示产品明细 */
function txnTypeHasProducts(txnType?: string) {
return txnType === 'purchase' || txnType === 'unsubscribe'
return txnType === 'purchase' || txnType === 'unsubscribe' || txnType === 'settlement'
}
function financeSubmitRemarkDisplay(d: Record<string, any>) {
@@ -545,7 +583,11 @@
/** 发起确认区附件:退款优先展示 refundAttachmentFiles */
function financeSubmitAttachmentDisplayFiles(d: Record<string, any>) {
if (d.txnType === 'refund' && Array.isArray(d.refundAttachmentFiles) && d.refundAttachmentFiles.length) {
if (
d.txnType === 'refund' &&
Array.isArray(d.refundAttachmentFiles) &&
d.refundAttachmentFiles.length
) {
return d.refundAttachmentFiles
}
return Array.isArray(d.financeSubmitAttachmentFiles) ? d.financeSubmitAttachmentFiles : []
@@ -700,7 +742,11 @@
fields: [
{ label: '流水类型', value: txnTypeLabel(d.txnType), tagType: txnTypeTagType(d.txnType) },
{ label: '金额(元)', value: `¥${formatAmount(d.amount)}` },
{ label: '状态', value: txnStatusLabel(d.txnStatus), tagType: txnStatusTagType(d.txnStatus) },
{
label: '状态',
value: txnStatusLabel(d.txnStatus),
tagType: txnStatusTagType(d.txnStatus)
},
{ label: '订单名称', value: d.orderName },
{ label: '渠道名称', value: d.channelName },
{ label: '创建人', value: d.createByName || d.createBy },
@@ -1006,7 +1052,6 @@
color: var(--el-color-primary);
}
.finance-receipt-dialog {
:deep(.el-dialog__body) {
padding-top: 16px;
File diff suppressed because it is too large. Load diff
@@ -8,7 +8,13 @@
class="list-table inner-table"
:max-height="maxHeight"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column label="类型" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="txnTypeTagType(row.txnType)" size="small">
@@ -29,7 +35,13 @@
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" header-align="center" />
<el-table-column
prop="createTime"
label="创建时间"
min-width="170"
align="center"
header-align="center"
/>
<el-table-column label="作废信息" min-width="200" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.txnStatus === 'yzf'" class="void-info">
@@ -44,51 +56,51 @@
</template>
<script setup lang="ts">
import {
txnTypeLabel,
txnTypeTagType,
txnStatusLabel,
txnStatusTagType
} from '@/utils/status-tag-registry'
import { formatMoneyDisplay } from '@/utils/money-format'
import {
txnTypeLabel,
txnTypeTagType,
txnStatusLabel,
txnStatusTagType
} from '@/utils/status-tag-registry'
import { formatMoneyDisplay } from '@/utils/money-format'
defineOptions({ name: 'ServiceOrderTxnTable' })
defineOptions({ name: 'ServiceOrderTxnTable' })
withDefaults(
defineProps<{
rows: Record<string, any>[]
maxHeight?: number | string
}>(),
{
rows: () => [],
maxHeight: 360
withDefaults(
defineProps<{
rows: Record<string, any>[]
maxHeight?: number | string
}>(),
{
rows: () => [],
maxHeight: 360
}
)
const formatAmount = (v: unknown): string => {
if (v == null || v === '') return '0.00'
return formatMoneyDisplay(v)
}
)
const formatAmount = (v: unknown): string => {
if (v == null || v === '') return '0.00'
return formatMoneyDisplay(v)
}
</script>
<style lang="scss" scoped>
.service-order-txn-table-wrap {
margin-top: 0;
padding: 0;
border: none;
}
.service-order-txn-table-wrap {
margin-top: 0;
padding: 0;
border: none;
}
.amount-text {
font-weight: 600;
color: var(--mint-primary, #10b981);
}
.amount-text {
font-weight: 600;
color: var(--mint-primary, #10b981);
}
.void-info {
font-size: 13px;
color: var(--mint-text-muted, #64748b);
.void-info {
font-size: 13px;
color: var(--mint-text-muted, #64748b);
&.muted {
color: #cbd5e1;
&.muted {
color: #cbd5e1;
}
}
}
</style>
File diff suppressed because it is too large. Load diff
@@ -1,411 +1,422 @@
<!-- 渠道信息 — 只读查看(样式对齐采集协议单 ProtocolViewDialog) -->
<template>
<div class="channel-view-dialog-root">
<el-dialog
:model-value="modelValue"
:title="dialogTitle"
width="1100px"
class="dialog-form-layout channel-view-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
@update:model-value="emit('update:modelValue', $event)"
@closed="handleClosed"
>
<div class="channel-view-body" :class="{ 'is-sub-loading': loading && channel }">
<template v-if="channel">
<div v-loading="loading" class="detail-tabs">
<div
v-for="tab in viewTabs"
:key="tab.key"
class="detail-tab"
:class="{ active: activeTab === tab.key }"
@click="scrollToSection(tab.key)"
>
{{ tab.label }}
</div>
</div>
<div class="kpi-strip">
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><OfficeBuilding /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">渠道名称</div>
<div class="kpi-value kpi-value--emphasis">{{ dash(channel.channelName) }}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><CollectionTag /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">渠道类型</div>
<div class="kpi-value">
<el-tag :type="channelTypeTagType(channel.channelType)" size="small">
{{ channelTypeLabel(channel.channelType) }}
</el-tag>
</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><User /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">联系人</div>
<div class="kpi-value">{{ dash(channel.contactName) }}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><Calendar /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">创建时间</div>
<div class="kpi-value">{{ dash(channel.createTime) }}</div>
</div>
</div>
</div>
<div id="view-section-basic" class="section-card section-card--white">
<div class="section-title"><h3>基础信息</h3></div>
<div class="data-grid data-grid--3">
<div class="data-item">
<div class="data-label">渠道名称</div>
<div class="data-value">{{ dash(channel.channelName) }}</div>
</div>
<div class="data-item">
<div class="data-label">统一社会信用代码</div>
<div class="data-value">{{ dash(channel.creditCode) }}</div>
</div>
<div class="data-item">
<div class="data-label">渠道类型</div>
<div class="data-value">
<el-tag :type="channelTypeTagType(channel.channelType)" size="small">
{{ channelTypeLabel(channel.channelType) }}
</el-tag>
</div>
</div>
<div class="data-item">
<div class="data-label">联系人</div>
<div class="data-value">{{ dash(channel.contactName) }}</div>
</div>
<div class="data-item">
<div class="data-label">联系电话</div>
<div class="data-value">{{ dash(channel.contactPhone) }}</div>
</div>
<div class="data-item">
<div class="data-label">联系邮箱</div>
<div class="data-value">{{ dash(channel.contactEmail) }}</div>
</div>
<div class="data-item">
<div class="data-label">创建时间</div>
<div class="data-value">{{ dash(channel.createTime) }}</div>
</div>
</div>
<div v-if="protocolAttachmentFiles.length" class="attachment-row">
<div class="data-label">渠道协议附件</div>
<ProtocolAttachmentReadonly
:files="protocolAttachmentFiles"
@preview="openPreview"
@download="downloadFile"
/>
</div>
<div v-if="channel.remark" class="remark-row">
<div class="data-item data-item--full">
<div class="data-label">备注</div>
<div class="data-value data-value--muted">{{ dash(channel.remark) }}</div>
</div>
</div>
</div>
<div id="view-section-invoice" class="section-card section-card--green">
<div class="section-title"><h3>开票信息</h3></div>
<div v-if="invoiceList.length" class="sub-cards">
<div v-for="(row, idx) in invoiceList" :key="row.id || idx" class="sub-card">
<div class="sub-card-header">
<h4>{{ row.companyName || '未命名' }}</h4>
</div>
<div class="data-grid data-grid--2">
<div v-if="row.taxNo" class="data-item">
<div class="data-label">税号</div>
<div class="data-value">{{ dash(row.taxNo) }}</div>
</div>
<div v-if="row.phone || row.companyPhone" class="data-item">
<div class="data-label">电话</div>
<div class="data-value">{{ dash(row.phone || row.companyPhone) }}</div>
</div>
<div v-if="row.companyAddress" class="data-item data-item--full">
<div class="data-label">单位地址</div>
<div class="data-value data-value--muted">{{ dash(row.companyAddress) }}</div>
</div>
<div v-if="row.bankName" class="data-item">
<div class="data-label">开户银行</div>
<div class="data-value">{{ dash(row.bankName) }}</div>
</div>
<div v-if="row.bankAccount" class="data-item">
<div class="data-label">银行账号</div>
<div class="data-value">{{ dash(row.bankAccount) }}</div>
</div>
</div>
</div>
</div>
<el-empty v-else description="暂无开票信息" :image-size="48" />
</div>
<div id="view-section-mail" class="section-card section-card--white">
<div class="section-title"><h3>邮寄地址</h3></div>
<div v-if="mailList.length" class="sub-cards">
<div v-for="(row, idx) in mailList" :key="row.id || idx" class="sub-card">
<div class="sub-card-header">
<h4>{{ row.mailAddress || '未命名' }}</h4>
</div>
<div class="data-grid data-grid--2">
<div v-if="row.mailContactName || row.mailContact" class="data-item">
<div class="data-label">联系人</div>
<div class="data-value">{{ dash(row.mailContactName || row.mailContact) }}</div>
</div>
<div v-if="row.mailContactPhone || row.mailPhone" class="data-item">
<div class="data-label">联系电话</div>
<div class="data-value">{{ dash(row.mailContactPhone || row.mailPhone) }}</div>
</div>
</div>
</div>
</div>
<el-empty v-else description="暂无邮寄地址" :image-size="48" />
</div>
<div id="view-section-account" class="section-card section-card--white">
<div class="section-title"><h3>账户信息</h3></div>
<div class="account-balance-card">
<div class="account-balance-card__label">账户余额</div>
<div class="account-balance-card__value">
¥ {{ accountLoading ? '—' : formatAmount(accountData?.balanceAmount) }}
</div>
</div>
<div class="account-quotas-head">
<h4 class="account-quotas-title">产品份数</h4>
</div>
<div v-if="quotasLoading" v-loading="true" style="min-height: 120px"></div>
<div v-else-if="productQuotas.length" class="list-body list-wrap account-quota-list-wrap">
<el-table
:data="productQuotas"
class="list-table account-quota-table"
stripe
row-key="id"
max-height="360"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="specText"
label="规格"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="totalQty"
label="总份数"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="usedQty"
label="已用份数"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="remainingQty"
label="剩余份数"
min-width="100"
align="center"
header-align="center"
/>
</el-table>
</div>
<el-empty v-else description="暂无产品份数数据" :image-size="48" />
</div>
<div id="view-section-agreement" class="section-card">
<div class="section-title"><h3>协议信息</h3></div>
<div v-if="agreementList.length" class="agreement-cards">
<el-dialog
:model-value="modelValue"
:title="dialogTitle"
width="1100px"
class="dialog-form-layout channel-view-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
@update:model-value="emit('update:modelValue', $event)"
@closed="handleClosed"
>
<div class="channel-view-body" :class="{ 'is-sub-loading': loading && channel }">
<template v-if="channel">
<div v-loading="loading" class="detail-tabs">
<div
v-for="(agr, idx) in agreementList"
:key="agr.id || idx"
class="agreement-card"
v-for="tab in viewTabs"
:key="tab.key"
class="detail-tab"
:class="{ active: activeTab === tab.key }"
@click="scrollToSection(tab.key)"
>
<div class="agreement-card__header">
<div class="agreement-card__tags">
<el-tag :type="agreementTypeTagType(agr.agreementType)" size="small">
{{ agreementTypeLabel(agr.agreementType) }}
</el-tag>
<el-tag :type="agreementStatusTagType(agr.status)" size="small">
{{ agreementStatusLabel(agr.status) }}
{{ tab.label }}
</div>
</div>
<div class="kpi-strip">
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><OfficeBuilding /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">渠道名称</div>
<div class="kpi-value kpi-value--emphasis">{{ dash(channel.channelName) }}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><CollectionTag /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">渠道类型</div>
<div class="kpi-value">
<el-tag :type="channelTypeTagType(channel.channelType)" size="small">
{{ channelTypeLabel(channel.channelType) }}
</el-tag>
</div>
</div>
<div class="data-grid data-grid--4 agreement-card__info">
<div v-if="agr.settlementType" class="data-item">
<div class="data-label">结算方式</div>
<div class="data-value">
<el-tag :type="settlementTypeTagType(agr.settlementType)" size="small">
{{ settlementTypeLabel(agr.settlementType) }}
</div>
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><User /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">联系人</div>
<div class="kpi-value">{{ dash(channel.contactName) }}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><Calendar /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">创建时间</div>
<div class="kpi-value">{{ dash(channel.createTime) }}</div>
</div>
</div>
</div>
<div id="view-section-basic" class="section-card section-card--white">
<div class="section-title"><h3>基础信息</h3></div>
<div class="data-grid data-grid--3">
<div class="data-item">
<div class="data-label">渠道名称</div>
<div class="data-value">{{ dash(channel.channelName) }}</div>
</div>
<div class="data-item">
<div class="data-label">统一社会信用代码</div>
<div class="data-value">{{ dash(channel.creditCode) }}</div>
</div>
<div class="data-item">
<div class="data-label">渠道类型</div>
<div class="data-value">
<el-tag :type="channelTypeTagType(channel.channelType)" size="small">
{{ channelTypeLabel(channel.channelType) }}
</el-tag>
</div>
</div>
<div class="data-item">
<div class="data-label">联系人</div>
<div class="data-value">{{ dash(channel.contactName) }}</div>
</div>
<div class="data-item">
<div class="data-label">联系电话</div>
<div class="data-value">{{ dash(channel.contactPhone) }}</div>
</div>
<div class="data-item">
<div class="data-label">联系邮箱</div>
<div class="data-value">{{ dash(channel.contactEmail) }}</div>
</div>
<div class="data-item">
<div class="data-label">创建时间</div>
<div class="data-value">{{ dash(channel.createTime) }}</div>
</div>
</div>
<div v-if="protocolAttachmentFiles.length" class="attachment-row">
<div class="data-label">渠道协议附件</div>
<ProtocolAttachmentReadonly
:files="protocolAttachmentFiles"
@preview="openPreview"
@download="downloadFile"
/>
</div>
<div v-if="channel.remark" class="remark-row">
<div class="data-item data-item--full">
<div class="data-label">备注</div>
<div class="data-value data-value--muted">{{ dash(channel.remark) }}</div>
</div>
</div>
</div>
<div id="view-section-invoice" class="section-card section-card--green">
<div class="section-title"><h3>开票信息</h3></div>
<div v-if="invoiceList.length" class="sub-cards">
<div v-for="(row, idx) in invoiceList" :key="row.id || idx" class="sub-card">
<div class="sub-card-header">
<h4>{{ row.companyName || '未命名' }}</h4>
</div>
<div class="data-grid data-grid--2">
<div v-if="row.taxNo" class="data-item">
<div class="data-label">税号</div>
<div class="data-value">{{ dash(row.taxNo) }}</div>
</div>
<div v-if="row.phone || row.companyPhone" class="data-item">
<div class="data-label">电话</div>
<div class="data-value">{{ dash(row.phone || row.companyPhone) }}</div>
</div>
<div v-if="row.companyAddress" class="data-item data-item--full">
<div class="data-label">单位地址</div>
<div class="data-value data-value--muted">{{ dash(row.companyAddress) }}</div>
</div>
<div v-if="row.bankName" class="data-item">
<div class="data-label">开户银行</div>
<div class="data-value">{{ dash(row.bankName) }}</div>
</div>
<div v-if="row.bankAccount" class="data-item">
<div class="data-label">银行账号</div>
<div class="data-value">{{ dash(row.bankAccount) }}</div>
</div>
</div>
</div>
</div>
<el-empty v-else description="暂无开票信息" :image-size="48" />
</div>
<div id="view-section-mail" class="section-card section-card--white">
<div class="section-title"><h3>邮寄地址</h3></div>
<div v-if="mailList.length" class="sub-cards">
<div v-for="(row, idx) in mailList" :key="row.id || idx" class="sub-card">
<div class="sub-card-header">
<h4>{{ row.mailAddress || '未命名' }}</h4>
</div>
<div class="data-grid data-grid--2">
<div v-if="row.mailContactName || row.mailContact" class="data-item">
<div class="data-label">联系人</div>
<div class="data-value">{{ dash(row.mailContactName || row.mailContact) }}</div>
</div>
<div v-if="row.mailContactPhone || row.mailPhone" class="data-item">
<div class="data-label">联系电话</div>
<div class="data-value">{{ dash(row.mailContactPhone || row.mailPhone) }}</div>
</div>
</div>
</div>
</div>
<el-empty v-else description="暂无邮寄地址" :image-size="48" />
</div>
<div id="view-section-account" class="section-card section-card--white">
<div class="section-title"><h3>账户信息</h3></div>
<div class="account-balance-card">
<div class="account-balance-card__label">账户余额</div>
<div class="account-balance-card__value">
¥ {{ accountLoading ? '—' : formatAmount(accountData?.balanceAmount) }}
</div>
</div>
<div class="account-quotas-head">
<h4 class="account-quotas-title">产品份数</h4>
</div>
<div v-if="quotasLoading" v-loading="true" style="min-height: 120px"></div>
<div
v-else-if="productQuotas.length"
class="list-body list-wrap account-quota-list-wrap"
>
<el-table
:data="productQuotas"
class="list-table account-quota-table"
stripe
row-key="id"
max-height="360"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="specText"
label="规格"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="totalQty"
label="总份数"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="usedQty"
label="已用份数"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="remainingQty"
label="剩余份数"
min-width="100"
align="center"
header-align="center"
/>
</el-table>
</div>
<el-empty v-else description="暂无产品份数数据" :image-size="48" />
</div>
<div id="view-section-agreement" class="section-card">
<div class="section-title"><h3>协议信息</h3></div>
<div v-if="agreementList.length" class="agreement-cards">
<div v-for="(agr, idx) in agreementList" :key="agr.id || idx" class="agreement-card">
<div class="agreement-card__header">
<div class="agreement-card__tags">
<el-tag :type="agreementTypeTagType(agr.agreementType)" size="small">
{{ agreementTypeLabel(agr.agreementType) }}
</el-tag>
<el-tag :type="agreementStatusTagType(agr.status)" size="small">
{{ agreementStatusLabel(agr.status) }}
</el-tag>
</div>
</div>
<div v-if="agr.creditLimit != null" class="data-item">
<div class="data-label">授信额度(元)</div>
<div class="data-value data-value--price">{{ dash(agr.creditLimit) }}</div>
<div class="data-grid data-grid--4 agreement-card__info">
<div v-if="agr.settlementType" class="data-item">
<div class="data-label">结算方式</div>
<div class="data-value">
<el-tag :type="settlementTypeTagType(agr.settlementType)" size="small">
{{ settlementTypeLabel(agr.settlementType) }}
</el-tag>
</div>
</div>
<div v-if="agr.creditLimit != null" class="data-item">
<div class="data-label">授信额度(元)</div>
<div class="data-value data-value--price">{{ dash(agr.creditLimit) }}</div>
</div>
<div v-if="agr.effectiveDate" class="data-item">
<div class="data-label">生效日期</div>
<div class="data-value">{{ dash(agr.effectiveDate) }}</div>
</div>
<div v-if="agr.expiryDate" class="data-item">
<div class="data-label">到期日期</div>
<div class="data-value">{{ dash(agr.expiryDate) }}</div>
</div>
</div>
<div v-if="agr.effectiveDate" class="data-item">
<div class="data-label">生效日期</div>
<div class="data-value">{{ dash(agr.effectiveDate) }}</div>
</div>
<div v-if="agr.expiryDate" class="data-item">
<div class="data-label">到期日期</div>
<div class="data-value">{{ dash(agr.expiryDate) }}</div>
</div>
</div>
<div class="agreement-card__products">
<div class="agreement-products-head">
<h4 class="agreement-products-title">细胞产品信息</h4>
</div>
<div
v-if="(agreementPricesMap[agr.id] || []).length > 0"
class="list-body list-wrap agreement-price-list-wrap"
>
<el-table
:data="agreementPricesMap[agr.id] || []"
class="list-table agreement-price-table"
stripe
row-key="id"
max-height="360"
<div class="agreement-card__products">
<div class="agreement-products-head">
<h4 class="agreement-products-title">细胞产品信息</h4>
</div>
<div
v-if="(agreementPricesMap[agr.id] || []).length > 0"
class="list-body list-wrap agreement-price-list-wrap"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="projectCode"
label="项目编码"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="specText"
label="规格"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
label="原价(元)"
min-width="100"
align="center"
header-align="center"
<el-table
:data="agreementPricesMap[agr.id] || []"
class="list-table agreement-price-table"
stripe
row-key="id"
max-height="360"
>
<template #default="{ row: priceRow }">
{{ formatPrice(priceRow.originalPrice) }}
</template>
</el-table-column>
<el-table-column
label="协议价(元)"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row: priceRow }">
<span class="data-value--price">{{ formatPrice(priceRow.agreementPrice) }}</span>
</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
</el-table>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="projectCode"
label="项目编码"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="specText"
label="规格"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
label="原价(元)"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row: priceRow }">
{{ formatPrice(priceRow.originalPrice) }}
</template>
</el-table-column>
<el-table-column
label="协议价(元)"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row: priceRow }">
<span class="data-value--price">{{
formatPrice(priceRow.agreementPrice)
}}</span>
</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
</el-table>
</div>
<el-empty
v-else
description="暂无细胞产品信息"
:image-size="48"
class="agreement-price-empty"
/>
</div>
<el-empty v-else description="暂无细胞产品信息" :image-size="48" class="agreement-price-empty" />
</div>
</div>
<el-empty v-else description="暂无协议信息" :image-size="48" />
</div>
<el-empty v-else description="暂无协议信息" :image-size="48" />
</div>
</template>
<div v-else v-loading="true" style="min-height: 200px"></div>
</div>
<template #footer>
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>
关闭
</el-button>
</template>
<div v-else v-loading="true" style="min-height: 200px"></div>
</div>
</el-dialog>
<template #footer>
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>
关闭
</el-button>
</template>
</el-dialog>
<el-dialog v-model="previewVisible" title="文件预览" width="800px" align-center destroy-on-close>
<div v-if="previewType === 'image'" class="preview-container">
<img
:src="previewUrl"
alt="预览"
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
<div v-else class="preview-container">
<video
:src="previewUrl"
controls
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
</el-dialog>
<el-dialog
v-model="previewVisible"
title="文件预览"
width="800px"
align-center
destroy-on-close
>
<div v-if="previewType === 'image'" class="preview-container">
<img
:src="previewUrl"
alt="预览"
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
<div v-else class="preview-container">
<video
:src="previewUrl"
controls
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
</el-dialog>
</div>
</template>
@@ -600,9 +611,7 @@
if (!agr.id) return [agr.id, []] as const
try {
const priceRes = await fetchChannelAgreementPrices(agr.id)
const prices = Array.isArray(priceRes)
? priceRes
: (priceRes as any)?.list || []
const prices = Array.isArray(priceRes) ? priceRes : (priceRes as any)?.list || []
return [agr.id, prices] as const
} catch {
return [agr.id, []] as const
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按渠道名称、统一社会信用代码模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -116,7 +120,13 @@
align="center"
header-align="center"
/>
<el-table-column label="操作" min-width="400" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="400"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
@@ -214,7 +224,11 @@
<el-form-item label="联系邮箱" prop="contactEmail">
<el-input v-model="formData.contactEmail" placeholder="请输入联系邮箱" />
</el-form-item>
<el-form-item label="渠道协议附件" class="form-col-span-2" style="margin-top: 18px">
<el-form-item
label="渠道协议附件"
class="form-col-span-2"
style="margin-top: 18px"
>
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
@@ -282,9 +296,7 @@
</div>
</div>
</div>
<span class="upload-hint"
>支持图片、视频、PDF、Word、Excel等文件</span
>
<span class="upload-hint">支持图片、视频、PDF、Word、Excel等文件</span>
</div>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
@@ -304,12 +316,7 @@
<div class="section-card">
<div class="section-title">
<div class="section-title-left"><h3>开票信息</h3></div>
<el-button
class="btn-main"
type="primary"
size="small"
@click="handleAddInvoice"
>
<el-button class="btn-main" type="primary" size="small" @click="handleAddInvoice">
<el-icon><Plus /></el-icon>
新增
</el-button>
@@ -320,11 +327,21 @@
v-loading="!!deletingInvoiceId"
element-loading-text="删除中…"
>
<div v-for="(row, $index) in invoiceList" :key="row.id || $index" class="sub-card">
<div
v-for="(row, $index) in invoiceList"
:key="row.id || $index"
class="sub-card"
>
<div class="sub-card-header">
<h4>{{ row.companyName || '未命名' }}</h4>
<div class="sub-card-actions">
<el-button class="btn-op-item primary" link size="small" @click="handleEditInvoice(row)">编辑</el-button>
<el-button
class="btn-op-item primary"
link
size="small"
@click="handleEditInvoice(row)"
>编辑</el-button
>
<el-button
class="btn-op-item"
type="danger"
@@ -371,12 +388,7 @@
<div class="section-card">
<div class="section-title">
<div class="section-title-left"><h3>邮寄地址</h3></div>
<el-button
class="btn-main"
type="primary"
size="small"
@click="handleAddMail"
>
<el-button class="btn-main" type="primary" size="small" @click="handleAddMail">
<el-icon><Plus /></el-icon>
新增
</el-button>
@@ -391,7 +403,13 @@
<div class="sub-card-header">
<h4>{{ row.mailAddress || '未命名' }}</h4>
<div class="sub-card-actions">
<el-button class="btn-op-item primary" link size="small" @click="handleEditMail(row)">编辑</el-button>
<el-button
class="btn-op-item primary"
link
size="small"
@click="handleEditMail(row)"
>编辑</el-button
>
<el-button
class="btn-op-item"
type="danger"
@@ -408,7 +426,9 @@
<div class="sub-card-fields">
<div v-if="row.mailContactName || row.mailContact" class="sub-card-field">
<span class="field-label">联系人</span>
<span class="field-value">{{ row.mailContactName || row.mailContact }}</span>
<span class="field-value">{{
row.mailContactName || row.mailContact
}}</span>
</div>
<div v-if="row.mailContactPhone || row.mailPhone" class="sub-card-field">
<span class="field-label">联系电话</span>
@@ -420,7 +440,6 @@
<el-empty v-else description="暂无邮寄地址" :image-size="48" />
</div>
</div>
</el-form>
</div>
@@ -469,7 +488,12 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleSubmitInvoice" :loading="invoiceSubmitLoading">
<el-button
class="btn-main"
type="primary"
@click="handleSubmitInvoice"
:loading="invoiceSubmitLoading"
>
<el-icon><Check /></el-icon>
确定
</el-button>
@@ -503,7 +527,10 @@
<el-input v-model="mailFormData.mailContactName" placeholder="请输入邮寄联系人" />
</el-form-item>
<el-form-item label="邮寄联系电话" prop="mailContactPhone">
<el-input v-model="mailFormData.mailContactPhone" placeholder="请输入邮寄联系电话" />
<el-input
v-model="mailFormData.mailContactPhone"
placeholder="请输入邮寄联系电话"
/>
</el-form-item>
</div>
</el-form>
@@ -512,7 +539,12 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleSubmitMail" :loading="mailSubmitLoading">
<el-button
class="btn-main"
type="primary"
@click="handleSubmitMail"
:loading="mailSubmitLoading"
>
<el-icon><Check /></el-icon>
确定
</el-button>
@@ -544,12 +576,7 @@
<div class="section-card" style="border: none; box-shadow: none; padding: 0">
<div class="section-title">
<div class="section-title-left"><h3>协议列表</h3></div>
<el-button
class="btn-main"
type="primary"
size="small"
@click="handleAddAgreement"
>
<el-button class="btn-main" type="primary" size="small" @click="handleAddAgreement">
<el-icon><Plus /></el-icon>
新增协议
</el-button>
@@ -568,16 +595,10 @@
<!-- 卡片上半部分:协议信息 -->
<div class="protocol-card__header">
<div class="protocol-card__tags">
<el-tag
:type="agreementTypeTagType(agr.agreementType)"
size="small"
>
<el-tag :type="agreementTypeTagType(agr.agreementType)" size="small">
{{ agreementTypeLabel(agr.agreementType) }}
</el-tag>
<el-tag
:type="agreementStatusTagType(agr.status)"
size="small"
>
<el-tag :type="agreementStatusTagType(agr.status)" size="small">
{{ agreementStatusLabel(agr.status) }}
</el-tag>
</div>
@@ -585,20 +606,36 @@
<el-button
v-if="agr.status === 'dsx'"
class="btn-op-item primary"
:loading="agreementOperatingId === agr.id && agreementOperatingAction === 'activate'"
:loading="
agreementOperatingId === agr.id && agreementOperatingAction === 'activate'
"
:disabled="!!agreementOperatingId"
@click="handleActivateAgreement(agr)"
>
<el-icon v-if="!(agreementOperatingId === agr.id && agreementOperatingAction === 'activate')"><Check /></el-icon>生效
<el-icon
v-if="
!(
agreementOperatingId === agr.id && agreementOperatingAction === 'activate'
)
"
><Check /></el-icon
>生效
</el-button>
<el-button
v-if="agr.status === 'dsx' || agr.status === 'sxz'"
class="btn-op-item"
:loading="agreementOperatingId === agr.id && agreementOperatingAction === 'void'"
:loading="
agreementOperatingId === agr.id && agreementOperatingAction === 'void'
"
:disabled="!!agreementOperatingId"
@click="handleVoidAgreement(agr)"
>
<el-icon v-if="!(agreementOperatingId === agr.id && agreementOperatingAction === 'void')"><CircleClose /></el-icon>作废
<el-icon
v-if="
!(agreementOperatingId === agr.id && agreementOperatingAction === 'void')
"
><CircleClose /></el-icon
>作废
</el-button>
<el-button
class="btn-op-item"
@@ -610,11 +647,18 @@
</el-button>
<el-button
class="btn-op-item"
:loading="agreementOperatingId === agr.id && agreementOperatingAction === 'delete'"
:loading="
agreementOperatingId === agr.id && agreementOperatingAction === 'delete'
"
:disabled="!!agreementOperatingId"
@click="handleDeleteAgreement(agr)"
>
<el-icon v-if="!(agreementOperatingId === agr.id && agreementOperatingAction === 'delete')"><Delete /></el-icon>删除
<el-icon
v-if="
!(agreementOperatingId === agr.id && agreementOperatingAction === 'delete')
"
><Delete /></el-icon
>删除
</el-button>
</div>
</div>
@@ -622,17 +666,16 @@
<div v-if="agr.settlementType" class="sub-card-field">
<span class="field-label">结算方式</span>
<span class="field-value">
<el-tag
:type="settlementTypeTagType(agr.settlementType)"
size="small"
>
<el-tag :type="settlementTypeTagType(agr.settlementType)" size="small">
{{ settlementTypeLabel(agr.settlementType) }}
</el-tag>
</span>
</div>
<div v-if="agr.creditLimit != null" class="sub-card-field">
<span class="field-label">授信额度(元)</span>
<span class="field-value">{{ agr.creditLimit != null ? formatMoneyDisplay(agr.creditLimit) : '-' }}</span>
<span class="field-value">{{
agr.creditLimit != null ? formatMoneyDisplay(agr.creditLimit) : '-'
}}</span>
</div>
<div v-if="agr.effectiveDate" class="sub-card-field">
<span class="field-label">生效日期</span>
@@ -650,27 +693,27 @@
<div class="detail-section-head">
<h3 class="detail-section-title">项目信息</h3>
<div class="protocol-card__products-actions">
<el-dropdown
trigger="click"
popper-class="channel-agreement-price-dropdown"
@command="(cmd: string) => handleAddPriceCommandFor(cmd, agr)"
>
<el-button class="btn-main" type="primary" size="small">
<el-icon><Plus /></el-icon>
添加价格
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="fromPriceSystem"
>从价格体系添加</el-dropdown-item
>
<el-dropdown-item command="fromProjectSystem"
>从项目价格体系添加</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-dropdown
trigger="click"
popper-class="channel-agreement-price-dropdown"
@command="(cmd: string) => handleAddPriceCommandFor(cmd, agr)"
>
<el-button class="btn-main" type="primary" size="small">
<el-icon><Plus /></el-icon>
添加价格
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="fromPriceSystem"
>从价格体系添加</el-dropdown-item
>
<el-dropdown-item command="fromProjectSystem"
>从项目价格体系添加</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<div
v-if="(agreementPricesMap[agr.id] || []).length > 0"
@@ -728,7 +771,11 @@
header-align="center"
>
<template #default="{ row: priceRow }">
{{ priceRow.originalPrice != null ? formatMoneyDisplay(priceRow.originalPrice) : '-' }}
{{
priceRow.originalPrice != null
? formatMoneyDisplay(priceRow.originalPrice)
: '-'
}}
</template>
</el-table-column>
<el-table-column
@@ -777,7 +824,9 @@
:disabled="!!deletingAgreementPriceId"
@click="handleDeletePriceInline(priceRow, agr)"
>
<el-icon v-if="deletingAgreementPriceId !== priceRow.id"><Delete /></el-icon>
<el-icon v-if="deletingAgreementPriceId !== priceRow.id"
><Delete
/></el-icon>
删除
</el-button>
</template>
@@ -818,10 +867,7 @@
>
<div class="form-grid-2">
<el-form-item label="协议状态" class="form-col-span-2">
<el-tag
:type="agreementStatusTagType(agreementFormStatus)"
size="default"
>
<el-tag :type="agreementStatusTagType(agreementFormStatus)" size="default">
{{ agreementStatusLabel(agreementFormStatus) }}
</el-tag>
<span v-if="!agreementIsEdit" class="form-label-tip" style="margin-left: 8px">
@@ -829,7 +875,11 @@
</span>
</el-form-item>
<el-form-item label="协议类型" prop="agreementType">
<el-select v-model="agreementFormData.agreementType" placeholder="请选择协议类型" style="width: 100%">
<el-select
v-model="agreementFormData.agreementType"
placeholder="请选择协议类型"
style="width: 100%"
>
<el-option
v-for="(item, key) in agreementType"
:key="key"
@@ -839,7 +889,11 @@
</el-select>
</el-form-item>
<el-form-item label="结算方式" prop="settlementType">
<el-select v-model="agreementFormData.settlementType" placeholder="请选择结算方式" style="width: 100%">
<el-select
v-model="agreementFormData.settlementType"
placeholder="请选择结算方式"
style="width: 100%"
>
<el-option
v-for="(item, key) in settlementType"
:key="key"
@@ -890,7 +944,12 @@
<el-icon><Check /></el-icon>
生效
</el-button>
<el-button class="btn-main" type="primary" @click="handleSubmitAgreement" :loading="agreementSubmitLoading">
<el-button
class="btn-main"
type="primary"
@click="handleSubmitAgreement"
:loading="agreementSubmitLoading"
>
<el-icon><Check /></el-icon>
确定
</el-button>
@@ -908,7 +967,10 @@
append-to-body
:close-on-click-modal="false"
>
<div v-if="priceSelectorMode === 'project'" class="price-selector-filter price-selector-system-row">
<div
v-if="priceSelectorMode === 'project'"
class="price-selector-filter price-selector-system-row"
>
<span class="price-selector-system-label">选择项目价格体系</span>
<el-select
v-model="selectedProjectSystemId"
@@ -953,16 +1015,47 @@
header-align="center"
:selectable="priceSelectorRowSelectable"
/>
<el-table-column prop="projectName" label="项目名称" min-width="120" align="center" header-align="center" />
<el-table-column prop="projectCode" label="项目编码" min-width="100" align="center" header-align="center" />
<el-table-column prop="cellTypeName" label="细胞类型" min-width="100" align="center" header-align="center" />
<el-table-column prop="specText" label="规格" min-width="100" align="center" header-align="center" />
<el-table-column
prop="projectName"
label="项目名称"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="projectCode"
label="项目编码"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="specText"
label="规格"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column label="单价(元)" min-width="90" 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="120" align="center" header-align="center" show-overflow-tooltip />
<el-table-column
prop="remark"
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
</el-table>
<template #footer>
<el-button class="btn-ghost" @click="priceSelectorVisible = false">
@@ -1023,7 +1116,21 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, CircleClose, Search, View, Edit, Delete, Document, VideoPlay, Loading, Download, RefreshLeft } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
CircleClose,
Search,
View,
Edit,
Delete,
Document,
VideoPlay,
Loading,
Download,
RefreshLeft
} from '@element-plus/icons-vue'
import ChannelViewDialog from './ChannelViewDialog.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import { fetchUploadAttachment } from '@/api/business'
@@ -1094,7 +1201,13 @@
function normalizeMailRow(row: Record<string, any>) {
const mailContactName = String(row.mailContact ?? row.mailContactName ?? '').trim()
const mailContactPhone = String(row.mailPhone ?? row.mailContactPhone ?? '').trim()
return { ...row, mailContactName, mailContactPhone, mailContact: mailContactName, mailPhone: mailContactPhone }
return {
...row,
mailContactName,
mailContactPhone,
mailContact: mailContactName,
mailPhone: mailContactPhone
}
}
/** 渠道协议:对齐后端 camelCase 字段,供列表/表单/卡片展示 */
@@ -1368,13 +1481,7 @@
const list = priceSelectorList.value
if (!kw) return list
return list.filter((row) => {
const text = [
row.projectName,
row.projectCode,
row.cellTypeName,
row.specText,
row.remark
]
const text = [row.projectName, row.projectCode, row.cellTypeName, row.specText, row.remark]
.map((v) => String(v ?? '').toLowerCase())
.join(' ')
return text.includes(kw)
@@ -1547,7 +1654,9 @@
// ---------------------------------------------------------------------------
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm('确定要删除该渠道吗?关联的子表数据也将清除。', '提示', { type: 'warning' })
await ElMessageBox.confirm('确定要删除该渠道吗?关联的子表数据也将清除。', '提示', {
type: 'warning'
})
} catch {
return
}
@@ -1561,7 +1670,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条记录吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条记录吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteChannel(id)
@@ -1741,9 +1852,7 @@
if (editingId.value) {
await fetchCreateChannelMailAddress(editingId.value, payload)
} else {
mailList.value.push(
normalizeMailRow({ id: `_local_mail_${Date.now()}`, ...payload })
)
mailList.value.push(normalizeMailRow({ id: `_local_mail_${Date.now()}`, ...payload }))
}
}
ElMessage.success(mailIsEdit.value ? '更新成功' : '添加成功')
@@ -2132,9 +2241,7 @@
}
if (!toAdd.length) {
ElMessage.warning(
skippedCount > 0 ? '所选项目均已存在,不可重复添加' : '请选择要添加的项目'
)
ElMessage.warning(skippedCount > 0 ? '所选项目均已存在,不可重复添加' : '请选择要添加的项目')
return
}
@@ -2375,7 +2482,9 @@
padding: 22px 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
transition: transform 0.2s, box-shadow 0.2s;
transition:
transform 0.2s,
box-shadow 0.2s;
position: relative;
background: #fff;
@@ -23,10 +23,34 @@
@selection-change="(rows) => (selectedRows = rows)"
>
<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" />
<el-table-column prop="packageName" label="套餐名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="packageCode" label="套餐编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="itemCount" label="项目数" min-width="80" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="packageName"
label="套餐名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="packageCode"
label="套餐编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="itemCount"
label="项目数"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column label="合计费用(元)" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{
row.totalFee != null ? `¥${formatMoneyDisplay(row.totalFee)}` : '—'
@@ -34,7 +58,9 @@
</el-table-column>
<el-table-column prop="status" label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{ row.status == '1' ? '正常' : '停用' }}</el-tag>
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{
row.status == '1' ? '正常' : '停用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -44,7 +70,13 @@
align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEdit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
@@ -127,12 +159,14 @@
<span v-if="row.sourceProjectDeleted" class="form-label-tip">源项目已删除</span>
</template>
</el-table-column>
<el-table-column label="项目费用(元)" min-width="120" align="center" header-align="center">
<el-table-column
label="项目费用(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<AmountInput
v-model="row.projectFee"
:min="0"
/>
<AmountInput v-model="row.projectFee" :min="0" />
</template>
</el-table-column>
<el-table-column label="状态" min-width="90" align="center" header-align="center">
@@ -147,7 +181,13 @@
<el-input v-model="row.remark" placeholder="可选" clearable />
</template>
</el-table-column>
<el-table-column label="操作" min-width="160" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="160"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ $index }">
<el-button class="btn-op-item" @click="detailItems.splice($index, 1)">
<el-icon><Delete /></el-icon>删除
@@ -211,12 +251,23 @@
align="center"
header-align="center"
/>
<el-table-column label="项目费用(元)" min-width="120" align="center" header-align="center">
<el-table-column
label="项目费用(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.projectFee != null ? `¥${formatMoneyDisplay(row.projectFee)}` : '—'
}}</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="90" align="center" header-align="center">
<el-table-column
prop="status"
label="状态"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{
row.status == '1' ? '正常' : '停用'
@@ -413,7 +464,9 @@
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(`确定删除套餐「${row.packageName}」?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除套餐「${row.packageName}」?`, '提示', {
type: 'warning'
})
} catch {
return
}
@@ -68,21 +68,67 @@
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="institutionName" label="机构名称" min-width="160" align="center" header-align="center" />
<el-table-column prop="institutionAddress" label="机构地址" min-width="180" align="center" header-align="center" />
<el-table-column prop="contactName" label="联系人" min-width="100" align="center" header-align="center" />
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" header-align="center" />
<el-table-column prop="status" label="状态" min-width="90" align="center" header-align="center">
<el-table-column
prop="institutionName"
label="机构名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="institutionAddress"
label="机构地址"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
prop="contactName"
label="联系人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="contactPhone"
label="联系电话"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="status"
label="状态"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{ row.status == '1' ? '正常' : '停用' }}</el-tag>
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{
row.status == '1' ? '正常' : '停用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="400" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="400"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item primary" @click="openItemDialog(row)"><el-icon><Setting /></el-icon>项目价格配置</el-button>
<el-button class="btn-op-item" @click="handleEdit(row)"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)"><el-icon><Delete /></el-icon>删除</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="openItemDialog(row)"
><el-icon><Setting /></el-icon>项目价格配置</el-button
>
<el-button class="btn-op-item" @click="handleEdit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDelete(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -109,7 +155,13 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form ref="companyFormRef" :model="companyForm" :rules="companyFormRules" label-position="top" class="biz-dialog-form">
<el-form
ref="companyFormRef"
:model="companyForm"
:rules="companyFormRules"
label-position="top"
class="biz-dialog-form"
>
<div class="form-grid-2">
<el-form-item label="机构名称" prop="institutionName">
<el-input v-model="companyForm.institutionName" placeholder="请输入机构名称" />
@@ -155,7 +207,12 @@
<el-button class="btn-ghost" @click="companyDialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="companySubmitLoading" @click="submitCompany">
<el-button
class="btn-main"
type="primary"
:loading="companySubmitLoading"
@click="submitCompany"
>
<el-icon><Check /></el-icon>确定
</el-button>
</template>
@@ -177,57 +234,109 @@
</div>
<el-tabs v-model="itemConfigTab" class="item-config-tabs">
<el-tab-pane label="项目列表" name="projects">
<div class="list-toolbar item-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ itemSelected.length }}</span> 项
<div class="list-toolbar item-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ itemSelected.length }}</span> 项
</div>
<el-button class="btn-main" type="primary" @click="handleAddItem">
<el-icon><Plus /></el-icon>新增项目
</el-button>
</div>
</div>
<div class="list-body list-wrap item-table-wrap">
<el-table
class="list-table"
:data="itemTable"
v-loading="itemLoading"
stripe
row-key="id"
max-height="460"
@selection-change="onItemSelectionChange"
>
<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"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="projectFee"
label="项目费用(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.projectFee != null ? `¥${formatMoneyDisplay(row.projectFee)}` : '—'
}}</template>
</el-table-column>
<el-table-column
prop="status"
label="状态"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{
row.status == '1' ? '正常' : '停用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEditItem(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDeleteItem(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="itemPagination.page"
:page-size="itemPagination.pageSize"
:total="itemPagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="
(v) => ((itemPagination.page = 1), (itemPagination.pageSize = v), loadItems())
"
@current-change="(v) => ((itemPagination.page = v), loadItems())"
/>
</div>
</div>
<el-button class="btn-main" type="primary" @click="handleAddItem">
<el-icon><Plus /></el-icon>新增项目
</el-button>
</div>
</div>
<div class="list-body list-wrap item-table-wrap">
<el-table
class="list-table"
:data="itemTable"
v-loading="itemLoading"
stripe
row-key="id"
max-height="460"
@selection-change="onItemSelectionChange"
>
<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" />
<el-table-column prop="projectName" label="项目名称" min-width="160" align="center" header-align="center" />
<el-table-column prop="projectFee" label="项目费用(元)" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ row.projectFee != null ? `¥${formatMoneyDisplay(row.projectFee)}` : '—' }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="90" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.status == '1' ? 'success' : 'info'" size="small">{{ row.status == '1' ? '正常' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" align="center" header-align="center" show-overflow-tooltip />
<el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEditItem(row)"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button class="btn-op-item" @click="handleDeleteItem(row)"><el-icon><Delete /></el-icon>删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="itemPagination.page"
:page-size="itemPagination.pageSize"
:total="itemPagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(v) => ((itemPagination.page = 1), (itemPagination.pageSize = v), loadItems())"
@current-change="(v) => ((itemPagination.page = v), loadItems())"
/>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="套餐管理" name="packages">
<CheckupPackagePanel
@@ -249,7 +358,13 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form ref="itemFormRef" :model="itemForm" :rules="itemFormRules" label-position="top" class="biz-dialog-form">
<el-form
ref="itemFormRef"
:model="itemForm"
:rules="itemFormRules"
label-position="top"
class="biz-dialog-form"
>
<div class="form-grid-2">
<el-form-item label="项目名称" prop="projectName">
<el-input v-model="itemForm.projectName" />
@@ -272,7 +387,12 @@
<el-button class="btn-ghost" @click="itemFormVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="itemSubmitLoading" @click="submitItem">
<el-button
class="btn-main"
type="primary"
:loading="itemSubmitLoading"
@click="submitItem"
>
<el-icon><Check /></el-icon>确定
</el-button>
</template>
@@ -547,7 +667,7 @@
invoiceTitle: companyForm.invoiceTitle?.trim() || undefined,
taxNo: companyForm.taxNo?.trim() || undefined,
bankName: companyForm.bankName?.trim() || undefined,
bankAccount: companyForm.bankAccount?.trim() || undefined,
bankAccount: companyForm.bankAccount?.trim() || undefined
}
await fetchCreateCheckupInstitutionInvoice((newInst as any).id, invoicePayload)
}
@@ -562,9 +682,13 @@
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(`确定删除「${row.institutionName}」?其下属项目价格将一并删除。`, '提示', {
type: 'warning'
})
await ElMessageBox.confirm(
`确定删除「${row.institutionName}」?其下属项目价格将一并删除。`,
'提示',
{
type: 'warning'
}
)
} catch {
return
}
@@ -588,7 +712,11 @@
{ label: '机构地址', value: row.institutionAddress, span: true },
{ label: '联系人', value: row.contactName },
{ label: '联系电话', value: row.contactPhone },
{ label: '状态', value: row.status == '1' ? '正常' : '停用', tagType: row.status == '1' ? 'success' : 'info' },
{
label: '状态',
value: row.status == '1' ? '正常' : '停用',
tagType: row.status == '1' ? 'success' : 'info'
},
{ label: '备注', value: row.remark, span: true }
]
}
@@ -15,19 +15,59 @@
<div class="section-card" style="margin-bottom: 16px">
<div class="section-title"><h3>基本信息</h3></div>
<div class="form-grid form-grid-2">
<div class="view-field"><span class="view-label">申请单号</span><span class="view-value">{{ checkupData.applicationNo || '—' }}</span></div>
<div class="view-field"><span class="view-label">患者姓名</span><span class="view-value">{{ checkupData.patientName || '—' }}</span></div>
<div class="view-field"><span class="view-label">性别</span><span class="view-value">{{ sexLabel(checkupData.sex) || '—' }}</span></div>
<div class="view-field"><span class="view-label">年龄</span><span class="view-value">{{ checkupData.age ?? '—' }}</span></div>
<div class="view-field"><span class="view-label">渠道名称</span><span class="view-value">{{ checkupData.channelName || '—' }}</span></div>
<div class="view-field"><span class="view-label">体检机构</span><span class="view-value">{{ checkupData.institutionName || '—' }}</span></div>
<div class="view-field"><span class="view-label">预约时间</span><span class="view-value">{{ checkupData.reserveTime || '—' }}</span></div>
<div class="view-field"><span class="view-label">预约费用</span><span class="view-value">{{ checkupData.reserveFee != null ? '¥' + checkupData.reserveFee.toFixed(2) : '—' }}</span></div>
<div class="view-field"><span class="view-label">实际体检时间</span><span class="view-value">{{ checkupData.actualCheckupTime || '—' }}</span></div>
<div class="view-field"><span class="view-label">实际费用</span><span class="view-value">{{ checkupData.actualCheckupFee != null ? '¥' + checkupData.actualCheckupFee.toFixed(2) : '—' }}</span></div>
<div class="view-field"
><span class="view-label">申请单号</span
><span class="view-value">{{ checkupData.applicationNo || '—' }}</span></div
>
<div class="view-field"
><span class="view-label">患者姓名</span
><span class="view-value">{{ checkupData.patientName || '—' }}</span></div
>
<div class="view-field"
><span class="view-label">性别</span
><span class="view-value">{{ sexLabel(checkupData.sex) || '—' }}</span></div
>
<div class="view-field"
><span class="view-label">年龄</span
><span class="view-value">{{ checkupData.age ?? '—' }}</span></div
>
<div class="view-field"
><span class="view-label">渠道名称</span
><span class="view-value">{{ checkupData.channelName || '—' }}</span></div
>
<div class="view-field"
><span class="view-label">体检机构</span
><span class="view-value">{{ checkupData.institutionName || '—' }}</span></div
>
<div class="view-field"
><span class="view-label">预约时间</span
><span class="view-value">{{ checkupData.reserveTime || '—' }}</span></div
>
<div class="view-field"
><span class="view-label">预约费用</span
><span class="view-value">{{
checkupData.reserveFee != null ? '¥' + checkupData.reserveFee.toFixed(2) : '—'
}}</span></div
>
<div class="view-field"
><span class="view-label">实际体检时间</span
><span class="view-value">{{ checkupData.actualCheckupTime || '—' }}</span></div
>
<div class="view-field"
><span class="view-label">实际费用</span
><span class="view-value">{{
checkupData.actualCheckupFee != null
? '¥' + checkupData.actualCheckupFee.toFixed(2)
: '—'
}}</span></div
>
<div class="view-field">
<span class="view-label">体检状态</span>
<span class="view-value"><el-tag :type="checkupStatusTagType(checkupData.checkupStatus)" size="small">{{ checkupStatusLabel(checkupData.checkupStatus) }}</el-tag></span>
<span class="view-value"
><el-tag :type="checkupStatusTagType(checkupData.checkupStatus)" size="small">{{
checkupStatusLabel(checkupData.checkupStatus)
}}</el-tag></span
>
</div>
</div>
</div>
@@ -39,7 +79,9 @@
<el-table-column type="index" label="序号" min-width="60" align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="200" />
<el-table-column prop="projectFee" label="项目费用(元)" min-width="120" align="center">
<template #default="{ row }">{{ row.projectFee != null ? row.projectFee.toFixed(2) : '—' }}</template>
<template #default="{ row }">{{
row.projectFee != null ? row.projectFee.toFixed(2) : '—'
}}</template>
</el-table-column>
</el-table>
<div style="margin-top: 8px; font-size: 13px; color: #334155">
@@ -59,23 +101,40 @@
<el-table-column type="index" label="序号" min-width="60" align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="200">
<template #default="{ row }">
<el-input v-if="row._editing" v-model="row.projectName" size="small" placeholder="请输入项目名称" />
<el-input
v-if="row._editing"
v-model="row.projectName"
size="small"
placeholder="请输入项目名称"
/>
<span v-else>{{ row.projectName }}</span>
</template>
</el-table-column>
<el-table-column prop="projectFee" label="项目费用(元)" min-width="140" align="center">
<template #default="{ row }">
<el-input-number v-if="row._editing" v-model="row.projectFee" size="small" :min="0" :precision="2" controls-position="right" style="width: 120px" />
<el-input-number
v-if="row._editing"
v-model="row.projectFee"
size="small"
:min="0"
:precision="2"
controls-position="right"
style="width: 120px"
/>
<span v-else>{{ row.projectFee != null ? row.projectFee.toFixed(2) : '—' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="140" align="center">
<template #default="{ row, $index }">
<template v-if="row._editing">
<el-button link type="primary" @click="saveActualItem(row, $index)"><el-icon><Check /></el-icon>保存</el-button>
<el-button link type="primary" @click="saveActualItem(row, $index)"
><el-icon><Check /></el-icon>保存</el-button
>
</template>
<template v-else>
<el-button link type="danger" @click="deleteActualItem(row, $index)"><el-icon><Delete /></el-icon>删除</el-button>
<el-button link type="danger" @click="deleteActualItem(row, $index)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</template>
</el-table-column>
@@ -124,8 +183,12 @@
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
<div class="attachment-actions">
<el-icon class="action-btn" @click.stop="downloadReport(file)"><Download /></el-icon>
<el-icon class="action-btn action-btn--danger" @click.stop="removeReport(idx)"><Delete /></el-icon>
<el-icon class="action-btn" @click.stop="downloadReport(file)"
><Download
/></el-icon>
<el-icon class="action-btn action-btn--danger" @click.stop="removeReport(idx)"
><Delete
/></el-icon>
</div>
</div>
</div>
@@ -163,7 +226,14 @@
import { ref, computed, watch, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Plus, Close, Check, Delete, Download, Document, Loading, CircleCheck
Plus,
Close,
Check,
Delete,
Download,
Document,
Loading,
CircleCheck
} from '@element-plus/icons-vue'
import {
fetchPilotCheckupDetail,
@@ -201,13 +271,17 @@
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
function fullFileUrl(suffix: string) {
if (!suffix) return ''
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/')) return suffix
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/'))
return suffix
return `${minioBaseUrl}/${suffix}`
}
const isImageFile = (file: any) => {
const url = (file.fileUrl || '').toLowerCase()
const name = (file.fileName || '').toLowerCase()
return /\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/.test(url) || /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(name)
return (
/\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/.test(url) ||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(name)
)
}
// ── 加载数据 ──────────────────────────
@@ -227,7 +301,9 @@
const reserveData = reserveRes?.data ?? reserveRes
reserveItems.value = Array.isArray(reserveData) ? reserveData : (reserveData?.list ?? [])
const actualData = actualRes?.data ?? actualRes
actualItems.value = (Array.isArray(actualData) ? actualData : (actualData?.list ?? [])).map((i: any) => ({ ...i, _editing: false }))
actualItems.value = (Array.isArray(actualData) ? actualData : (actualData?.list ?? [])).map(
(i: any) => ({ ...i, _editing: false })
)
// 加载主信息 — 通过 checkup id 获取
// fetchPilotCheckupDetail 接受 application_id 参数
@@ -266,7 +342,10 @@
items
})
const data = res?.data ?? res
actualItems.value = (Array.isArray(data) ? data : (data?.list ?? [])).map((i: any) => ({ ...i, _editing: false }))
actualItems.value = (Array.isArray(data) ? data : (data?.list ?? [])).map((i: any) => ({
...i,
_editing: false
}))
ElMessage.success('保存成功')
} catch (e: any) {
ElMessage.error(e?.message || '保存失败')
@@ -295,7 +374,11 @@
if (!file) return
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
reportFiles.value.push({
id: tempId, fileName: file.name, fileUrl: '', fileSize: file.size, _uploading: true
id: tempId,
fileName: file.name,
fileUrl: '',
fileSize: file.size,
_uploading: true
})
const idx = reportFiles.value.length - 1
try {
@@ -337,7 +420,9 @@
ElMessage.success('标记成功')
emit('success')
emit('update:modelValue', false)
} catch { /* cancelled */ }
} catch {
/* cancelled */
}
}
// ── 保存 ──────────────────────────────
@@ -345,7 +430,10 @@
try {
await savePilotCheckup({
application_id: checkupData.value.pilotApplicationId,
report_attachments: reportFiles.value.map((f) => f.fileUrl).filter(Boolean).join(','),
report_attachments: reportFiles.value
.map((f) => f.fileUrl)
.filter(Boolean)
.join(','),
checkup_remark: checkupRemark.value
})
ElMessage.success('保存成功')
@@ -367,74 +455,163 @@
</script>
<style lang="scss" scoped>
.view-field {
display: flex;
align-items: baseline;
gap: 8px;
min-height: 32px;
}
.view-label {
font-size: 13px;
color: #94a3b8;
white-space: nowrap;
flex-shrink: 0;
&::after {
content: ':';
.view-field {
display: flex;
align-items: baseline;
gap: 8px;
min-height: 32px;
}
.view-label {
font-size: 13px;
color: #94a3b8;
white-space: nowrap;
flex-shrink: 0;
&::after {
content: ':';
}
}
.view-value {
font-size: 13px;
color: #334155;
word-break: break-all;
}
}
.view-value {
font-size: 13px;
color: #334155;
word-break: break-all;
}
.attachment-upload { width: 100%; }
.attachment-list { display: flex; flex-wrap: wrap; gap: 12px; }
.upload-card {
width: 140px; height: 96px;
border: 1px dashed #a7f3d0; border-radius: 8px;
background: #f0fdf4;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 6px; cursor: pointer;
transition: border-color 0.15s, background 0.15s;
&:hover { border-color: #10b981; border-style: solid; background: #ecfdf5; }
}
.upload-card-text { font-size: 12px; color: #10b981; font-weight: 500; line-height: 1; }
.upload-hint { display: block; font-size: 11px; color: #94a3b8; line-height: 1; margin-top: 8px; }
.attachment-item {
width: 140px; display: flex; flex-direction: column;
border: 1px solid #e2e8f0; border-radius: 8px; background: #fff;
overflow: hidden; transition: border-color 0.15s, box-shadow 0.15s;
&:hover { border-color: #10b981; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1); }
&.is-uploading { opacity: 0.6; pointer-events: none; }
}
.attachment-preview {
width: 100%; height: 96px; flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
background: #f8fafc; overflow: hidden;
img { width: 100%; height: 100%; object-fit: cover; display: block; }
}
.attachment-preview__icon {
width: 48px; height: 48px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
&--doc { background: #e0f2fe; color: #0284c7; }
}
.attachment-footer { display: flex; flex-direction: column; flex: 1; padding: 6px 8px; }
.attachment-name {
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden; text-overflow: ellipsis;
font-size: 11px; color: #334155; line-height: 1.4; word-break: break-all;
}
.attachment-actions {
display: flex; justify-content: center; align-items: center;
gap: 8px; margin-top: 4px; flex-shrink: 0;
}
.action-btn {
width: 22px; height: 22px; border-radius: 4px;
display: flex; align-items: center; justify-content: center;
font-size: 12px; color: #64748b; cursor: pointer; background: #f1f5f9;
transition: background 0.15s, color 0.15s;
&:hover { background: #10b981; color: #fff; }
&--danger:hover { background: #ef4444; color: #fff; }
}
.attachment-upload {
width: 100%;
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.upload-card {
width: 140px;
height: 96px;
border: 1px dashed #a7f3d0;
border-radius: 8px;
background: #f0fdf4;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
transition:
border-color 0.15s,
background 0.15s;
&:hover {
border-color: #10b981;
border-style: solid;
background: #ecfdf5;
}
}
.upload-card-text {
font-size: 12px;
color: #10b981;
font-weight: 500;
line-height: 1;
}
.upload-hint {
display: block;
font-size: 11px;
color: #94a3b8;
line-height: 1;
margin-top: 8px;
}
.attachment-item {
width: 140px;
display: flex;
flex-direction: column;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
overflow: hidden;
transition:
border-color 0.15s,
box-shadow 0.15s;
&:hover {
border-color: #10b981;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1);
}
&.is-uploading {
opacity: 0.6;
pointer-events: none;
}
}
.attachment-preview {
width: 100%;
height: 96px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.attachment-preview__icon {
width: 48px;
height: 48px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
&--doc {
background: #e0f2fe;
color: #0284c7;
}
}
.attachment-footer {
display: flex;
flex-direction: column;
flex: 1;
padding: 6px 8px;
}
.attachment-name {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
font-size: 11px;
color: #334155;
line-height: 1.4;
word-break: break-all;
}
.attachment-actions {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 4px;
flex-shrink: 0;
}
.action-btn {
width: 22px;
height: 22px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #64748b;
cursor: pointer;
background: #f1f5f9;
transition:
background 0.15s,
color 0.15s;
&:hover {
background: #10b981;
color: #fff;
}
&--danger:hover {
background: #ef4444;
color: #fff;
}
}
</style>
@@ -115,9 +115,26 @@
header-align="center"
:selectable="(row) => !isProjectAlreadyInReserve(row)"
/>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="180" align="center" header-align="center" />
<el-table-column label="项目费用(元)" min-width="120" align="center" header-align="center">
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
label="项目费用(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.projectFee != null ? formatMoneyDisplay(row.projectFee) : '—'
}}</template>
@@ -184,13 +201,42 @@
@selection-change="(rows) => (packageSelectorSelected = rows)"
>
<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" />
<el-table-column prop="packageName" label="套餐名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="packageCode" label="套餐编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="itemCount" label="项目数" min-width="80" align="center" header-align="center">
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="packageName"
label="套餐名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="packageCode"
label="套餐编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="itemCount"
label="项目数"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.itemCount ?? '—' }}</template>
</el-table-column>
<el-table-column label="合计费用(元)" min-width="120" align="center" header-align="center">
<el-table-column
label="合计费用(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.totalFee != null ? formatMoneyDisplay(row.totalFee) : '—'
}}</template>
@@ -210,7 +256,13 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" min-width="100" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="100"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handlePackageView(row)">
<el-icon><View /></el-icon>查看
@@ -252,8 +304,20 @@
stripe
max-height="420"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="180" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column label="项目费用(元)" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{
row.projectFee != null ? formatMoneyDisplay(row.projectFee) : '—'
@@ -404,9 +468,7 @@
const filteredProjectOptions = computed(() => {
const kw = projectSelectorKeyword.value.trim().toLowerCase()
if (!kw) return projectOptions.value
return projectOptions.value.filter((row) =>
(row.projectName || '').toLowerCase().includes(kw)
)
return projectOptions.value.filter((row) => (row.projectName || '').toLowerCase().includes(kw))
})
const handleOpenProjectSelector = async () => {
@@ -46,7 +46,13 @@
stripe
max-height="360"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
@@ -55,7 +61,12 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="项目费用(元)" min-width="150" align="center" header-align="center">
<el-table-column
label="项目费用(元)"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<AmountInput v-model="row.projectFee" :min="0" placeholder="请输入费用" />
</template>
@@ -114,8 +125,12 @@
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
<div class="attachment-actions">
<el-icon class="action-btn" @click.stop="downloadReport(file)"><Download /></el-icon>
<el-icon class="action-btn action-btn--danger" @click.stop="removeReport(idx)"><Delete /></el-icon>
<el-icon class="action-btn" @click.stop="downloadReport(file)"
><Download
/></el-icon>
<el-icon class="action-btn action-btn--danger" @click.stop="removeReport(idx)"
><Delete
/></el-icon>
</div>
</div>
</div>
@@ -145,7 +160,13 @@
import { ref, computed, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import {
Plus, Close, CircleCheck, Delete, Download, Document, Loading
Plus,
Close,
CircleCheck,
Delete,
Download,
Document,
Loading
} from '@element-plus/icons-vue'
import {
fetchPilotCheckupDetail,
@@ -206,7 +227,10 @@
const isImageFile = (file: any) => {
const url = (file.fileUrl || '').toLowerCase()
const name = (file.fileName || '').toLowerCase()
return /\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/.test(url) || /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(name)
return (
/\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/.test(url) ||
/\.(jpg|jpeg|png|gif|bmp|webp|svg)$/.test(name)
)
}
const parseAttachmentUrls = (urls: string) => {
@@ -235,8 +259,8 @@
actualCheckupTime.value = detail.actualCheckupTime || formatNowDatetime()
reportFiles.value = parseAttachmentUrls(detail.reportAttachments || '')
const reserveList = Array.isArray(reserveRes) ? reserveRes : reserveRes?.list ?? []
const actualList = Array.isArray(actualRes) ? actualRes : actualRes?.list ?? []
const reserveList = Array.isArray(reserveRes) ? reserveRes : (reserveRes?.list ?? [])
const actualList = Array.isArray(actualRes) ? actualRes : (actualRes?.list ?? [])
if (actualList.length > 0) {
actualItems.value = dedupeCheckupActualItems(
@@ -261,7 +285,10 @@
if (!file) return
const tempId = `temp_${Date.now()}`
reportFiles.value.push({
id: tempId, fileName: file.name, fileUrl: '', _uploading: true
id: tempId,
fileName: file.name,
fileUrl: '',
_uploading: true
})
const idx = reportFiles.value.length - 1
try {
@@ -335,7 +362,10 @@
await savePilotCheckup({
checkupId: props.checkupId,
actualCheckupTime: actualCheckupTime.value,
reportAttachments: reportFiles.value.map((f) => f.fileUrl).filter(Boolean).join(','),
reportAttachments: reportFiles.value
.map((f) => f.fileUrl)
.filter(Boolean)
.join(','),
checkupRemark: checkupRemark.value || undefined,
actualCheckupFee: actualTotal.value || undefined
})
@@ -17,20 +17,64 @@
<h3>{{ isFillFromListMode ? '已选患者' : '选择患者' }}</h3>
</div>
<template v-if="isFillFromListMode">
<el-table :data="listModePatients" class="list-table" stripe row-key="id" max-height="240">
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column prop="applicationNo" label="申请单号" min-width="160" align="center" header-align="center" />
<el-table-column prop="patientName" label="患者姓名" min-width="100" align="center" header-align="center" />
<el-table-column prop="sex" label="性别" min-width="60" align="center" header-align="center">
<el-table
:data="listModePatients"
class="list-table"
stripe
row-key="id"
max-height="240"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="applicationNo"
label="申请单号"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="patientName"
label="患者姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="sex"
label="性别"
min-width="60"
align="center"
header-align="center"
>
<template #default="{ row }">{{ sexLabel(row.sex) || '—' }}</template>
</el-table-column>
<el-table-column prop="age" label="年龄" min-width="60" align="center" header-align="center">
<el-table-column
prop="age"
label="年龄"
min-width="60"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.age ?? '—' }}</template>
</el-table-column>
<el-table-column prop="channelName" label="渠道名称" min-width="140" align="center" header-align="center" />
<el-table-column
prop="channelName"
label="渠道名称"
min-width="140"
align="center"
header-align="center"
/>
</el-table>
<div style="margin-top: 8px; font-size: 13px; color: #64748b">
共 <strong style="color: #10b981">{{ listModePatients.length }}</strong> 名患者,将统一预约相同机构、时间与体检项目
共
<strong style="color: #10b981">{{ listModePatients.length }}</strong>
名患者,将统一预约相同机构、时间与体检项目
</div>
</template>
<template v-else>
@@ -61,19 +105,57 @@
@selection-change="(rows) => (selectedPatients = rows)"
>
<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" />
<el-table-column prop="applicationNo" label="申请单号" min-width="160" align="center" header-align="center" />
<el-table-column prop="patientName" label="患者姓名" min-width="100" align="center" header-align="center" />
<el-table-column prop="sex" label="性别" min-width="60" align="center" header-align="center">
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="applicationNo"
label="申请单号"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="patientName"
label="患者姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="sex"
label="性别"
min-width="60"
align="center"
header-align="center"
>
<template #default="{ row }">{{ sexLabel(row.sex) || '—' }}</template>
</el-table-column>
<el-table-column prop="age" label="年龄" min-width="60" align="center" header-align="center">
<el-table-column
prop="age"
label="年龄"
min-width="60"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.age ?? '—' }}</template>
</el-table-column>
<el-table-column prop="channelName" label="渠道名称" min-width="140" align="center" header-align="center" />
<el-table-column
prop="channelName"
label="渠道名称"
min-width="140"
align="center"
header-align="center"
/>
</el-table>
<div style="margin-top: 8px; font-size: 13px; color: #64748b">
已选 <strong style="color: #10b981">{{ selectedPatients.length }}</strong> 名患者(需体检且尚未登记体检记录)
已选
<strong style="color: #10b981">{{ selectedPatients.length }}</strong>
名患者(需体检且尚未登记体检记录)
</div>
</template>
</div>
@@ -183,9 +265,26 @@
@selection-change="(rows) => (projectSelectorSelected = rows)"
>
<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" />
<el-table-column prop="projectName" label="项目名称" min-width="180" align="center" header-align="center" />
<el-table-column label="项目费用(元)" min-width="120" align="center" header-align="center">
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
label="项目费用(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.projectFee != null ? formatMoneyDisplay(row.projectFee) : '—'
}}</template>
@@ -252,13 +351,42 @@
@selection-change="(rows) => (packageSelectorSelected = rows)"
>
<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" />
<el-table-column prop="packageName" label="套餐名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="packageCode" label="套餐编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="itemCount" label="项目数" min-width="80" align="center" header-align="center">
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="packageName"
label="套餐名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="packageCode"
label="套餐编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="itemCount"
label="项目数"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.itemCount ?? '—' }}</template>
</el-table-column>
<el-table-column label="合计费用(元)" min-width="120" align="center" header-align="center">
<el-table-column
label="合计费用(元)"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.totalFee != null ? formatMoneyDisplay(row.totalFee) : '—'
}}</template>
@@ -278,7 +406,13 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" min-width="100" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="100"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handlePackageView(row)">
<el-icon><View /></el-icon>查看
@@ -320,8 +454,20 @@
stripe
max-height="420"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="180" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column label="项目费用(元)" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{
row.projectFee != null ? formatMoneyDisplay(row.projectFee) : '—'
@@ -12,14 +12,36 @@
/>
<div class="list-body list-wrap checkup-reserve-list-wrap">
<el-table :data="items" class="list-table" stripe :max-height="maxHeight">
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="160" align="center" header-align="center" />
<el-table-column label="原始项目费用(元)" min-width="130" align="center" header-align="center">
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
label="原始项目费用(元)"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.originalProjectFee != null ? formatMoneyDisplay(row.originalProjectFee) : '—'
}}</template>
</el-table-column>
<el-table-column label="预约项目费用(元)" min-width="150" align="center" header-align="center">
<el-table-column
label="预约项目费用(元)"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<AmountInput v-model="row.projectFee" :min="0" placeholder="请输入费用" />
</template>
@@ -21,9 +21,25 @@
class="list-table inner-table"
max-height="320"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column label="项目费用(元)" min-width="120" align="right" header-align="center">
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="200"
show-overflow-tooltip
/>
<el-table-column
label="项目费用(元)"
min-width="120"
align="right"
header-align="center"
>
<template #default="{ row }">
{{ row.projectFee != null ? `¥${formatMoneyDisplay(row.projectFee)}` : '—' }}
</template>
@@ -45,9 +61,25 @@
class="list-table inner-table"
max-height="320"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="200" show-overflow-tooltip />
<el-table-column label="项目费用(元)" min-width="120" align="right" header-align="center">
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="200"
show-overflow-tooltip
/>
<el-table-column
label="项目费用(元)"
min-width="120"
align="right"
header-align="center"
>
<template #default="{ row }">
{{ row.projectFee != null ? `¥${formatMoneyDisplay(row.projectFee)}` : '—' }}
</template>
@@ -62,7 +94,11 @@
<template #reports>
<div v-if="reportFiles.length" class="report-attachment-list">
<div v-for="(file, idx) in reportFiles" :key="file.url || idx" class="report-attachment-item">
<div
v-for="(file, idx) in reportFiles"
:key="file.url || idx"
class="report-attachment-item"
>
<div class="report-attachment-preview">
<img v-if="isImageUrl(file.url)" :src="fullFileUrl(file.url)" :alt="file.name" />
<div v-else class="report-attachment-preview__icon">
@@ -195,9 +231,7 @@
})
}
const reportFiles = computed(() =>
parseAttachmentUrls(viewData.value.reportAttachments || '')
)
const reportFiles = computed(() => parseAttachmentUrls(viewData.value.reportAttachments || ''))
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
function fullFileUrl(suffix: string) {
@@ -83,10 +83,7 @@ export function findDuplicateReserveItemNames(items: CheckupReserveItem[]): stri
return [...dups]
}
export function appendCheckupReserveItems(
target: CheckupReserveItem[],
projects: any[]
): number {
export function appendCheckupReserveItems(target: CheckupReserveItem[], projects: any[]): number {
let added = 0
for (const proj of projects) {
const candidate = toReserveCandidate(proj)
@@ -120,10 +117,7 @@ export type CheckupActualItem = {
projectFee: number | null
}
export function isSameActualItem(
a: { projectName: string },
b: { projectName: string }
): boolean {
export function isSameActualItem(a: { projectName: string }, b: { projectName: string }): boolean {
return isSameReserveItem(
{ projectName: a.projectName, projectId: undefined },
{ projectName: b.projectName, projectId: undefined }
@@ -16,16 +16,30 @@
<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-input
v-model="searchForm.keyword"
placeholder="申请单号/患者姓名/渠道名称"
clearable
/>
</el-form-item>
<el-form-item label="体检状态">
<el-select v-model="searchForm.checkupStatus" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.checkupStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="待体检" value="dtj" />
<el-option label="已体检" value="ytj" />
</el-select>
</el-form-item>
<el-form-item label="体检机构">
<el-select v-model="searchForm.checkupInstitutionId" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.checkupInstitutionId"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option
v-for="inst in institutionOptions"
:key="inst.id"
@@ -84,35 +98,104 @@
header-align="center"
:selectable="isRowSelectable"
/>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" :index="(i: number) => (pagination.page - 1) * pagination.pageSize + i + 1" />
<el-table-column prop="applicationNo" label="申请单号" min-width="160" align="center" header-align="center" />
<el-table-column prop="patientName" label="患者姓名" min-width="100" align="center" header-align="center" />
<el-table-column prop="sex" label="性别" min-width="60" align="center" header-align="center">
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="(i: number) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column
prop="applicationNo"
label="申请单号"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="patientName"
label="患者姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="sex"
label="性别"
min-width="60"
align="center"
header-align="center"
>
<template #default="{ row }">{{ sexLabel(row.sex) || '—' }}</template>
</el-table-column>
<el-table-column prop="age" label="年龄" min-width="60" align="center" header-align="center">
<el-table-column
prop="age"
label="年龄"
min-width="60"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.age ?? '—' }}</template>
</el-table-column>
<el-table-column prop="channelName" label="渠道名称" min-width="140" align="center" header-align="center">
<el-table-column
prop="channelName"
label="渠道名称"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.channelName || '—' }}</template>
</el-table-column>
<el-table-column prop="institutionName" label="体检机构" min-width="160" align="center" header-align="center">
<el-table-column
prop="institutionName"
label="体检机构"
min-width="160"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.institutionName || '—' }}</template>
</el-table-column>
<el-table-column prop="reserveTime" label="预约时间" min-width="160" align="center" header-align="center">
<el-table-column
prop="reserveTime"
label="预约时间"
min-width="160"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.reserveTime || '—' }}</template>
</el-table-column>
<el-table-column prop="checkupStatus" label="体检状态" min-width="100" align="center" header-align="center">
<el-table-column
prop="checkupStatus"
label="体检状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="checkupStatusTagType(row.checkupStatus)" size="small">
{{ checkupStatusLabel(row.checkupStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="actualCheckupFee" label="体检费用" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ row.actualCheckupFee != null ? '¥' + formatMoneyDisplay(row.actualCheckupFee) : '—' }}</template>
<el-table-column
prop="actualCheckupFee"
label="体检费用"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{
row.actualCheckupFee != null ? '¥' + formatMoneyDisplay(row.actualCheckupFee) : '—'
}}</template>
</el-table-column>
<el-table-column label="操作" min-width="400" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="400"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button
v-if="!row.institutionName && row.checkupStatus === 'dtj'"
@@ -186,12 +269,15 @@
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import {
RefreshLeft, Search, View, CircleCheck, Edit, EditPen, Calendar
RefreshLeft,
Search,
View,
CircleCheck,
Edit,
EditPen,
Calendar
} from '@element-plus/icons-vue'
import {
fetchPilotCheckupList,
fetchCheckupInstitutionList
} from '@/api/admin'
import { fetchPilotCheckupList, fetchCheckupInstitutionList } from '@/api/admin'
import { checkupStatusLabel, checkupStatusTagType, sexLabel } from '@/utils/status-tag-registry'
import { formatMoneyDisplay } from '@/utils/money-format'
import CheckupFillDialog from './CheckupFillDialog.vue'
@@ -258,7 +344,7 @@
const loadInstitutions = async () => {
try {
const res: any = await fetchCheckupInstitutionList({ page: 1, pageSize: 100, status: '1' })
institutionOptions.value = Array.isArray(res) ? res : res?.list ?? []
institutionOptions.value = Array.isArray(res) ? res : (res?.list ?? [])
} catch {
institutionOptions.value = []
}
@@ -0,0 +1,89 @@
<!-- 输注计划管理 — 取消弹窗 -->
<template>
<el-dialog
:model-value="modelValue"
:title="'取消输注计划 — ' + (patientName || '')"
width="500px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
@update:model-value="emit('update:modelValue', $event)"
>
<el-form label-position="top">
<el-form-item label="取消原因">
<el-input
v-model="cancelReason"
type="textarea"
:rows="4"
placeholder="请输入取消原因"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button
class="btn-main"
type="primary"
:loading="submitLoading"
:disabled="!cancelReason.trim()"
@click="handleSubmit"
>
<el-icon><CircleCheck /></el-icon>确认取消
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Close, CircleCheck } from '@element-plus/icons-vue'
import { updateInfusionScheduleStatus } from '@/api/admin'
const props = defineProps<{
modelValue: boolean
scheduleId: string
patientName?: string
}>()
const emit = defineEmits(['update:modelValue', 'success'])
const cancelReason = ref('')
const submitLoading = ref(false)
const handleSubmit = async () => {
if (!cancelReason.value.trim()) {
ElMessage.warning('请输入取消原因')
return
}
submitLoading.value = true
try {
await updateInfusionScheduleStatus({
scheduleId: props.scheduleId,
infusionStatus: 'yqx',
cancelReason: cancelReason.value.trim()
})
ElMessage.success('已取消输注计划')
emit('update:modelValue', false)
emit('success')
} catch {
// error handled by request util
} finally {
submitLoading.value = false
}
}
watch(
() => props.modelValue,
(val) => {
if (val) {
cancelReason.value = ''
}
}
)
</script>
@@ -0,0 +1,253 @@
<!-- 输注计划管理 — 查看弹窗 -->
<template>
<SystemViewDialog
v-model:visible="innerVisible"
:title="viewData.patientName || '输注计划详情'"
:icon="Document"
:code="viewData.applicationNo"
code-label="申请单号"
:sections="sections"
:loading="loading"
:grid-columns="3"
width="1080px"
append-to-body
>
<template #products>
<div class="list-body list-wrap view-inner-table-wrap">
<el-table
v-if="viewData.products && viewData.products.length"
:data="viewData.products"
stripe
class="list-table inner-table"
max-height="320"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="160"
show-overflow-tooltip
/>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="specText"
label="规格"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
label="协议价格(元)"
min-width="120"
align="right"
header-align="center"
>
<template #default="{ row }">
{{ row.agreementPrice != null ? `¥${formatMoneyDisplay(row.agreementPrice)}` : '—' }}
</template>
</el-table-column>
<el-table-column
label="实际价格(元)"
min-width="120"
align="right"
header-align="center"
>
<template #default="{ row }">
{{ row.actualPrice != null ? `¥${formatMoneyDisplay(row.actualPrice)}` : '—' }}
</template>
</el-table-column>
<el-table-column
prop="quantity"
label="数量"
min-width="80"
align="center"
header-align="center"
/>
</el-table>
<el-empty v-else description="暂无产品" :image-size="48" />
</div>
</template>
</SystemViewDialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Document } from '@element-plus/icons-vue'
import SystemViewDialog, {
type ViewSection,
type TagType
} from '@/components/business/system-view-dialog/index.vue'
import { fetchDictDataOptions, fetchInfusionScheduleMgmtDetail } from '@/api/admin'
import {
scheduleStatusLabel,
scheduleStatusTagType,
infusionStatusLabel,
infusionStatusTagType,
qcStatusLabel,
qcStatusTagType,
sexLabel
} from '@/utils/status-tag-registry'
import { formatMoneyDisplay } from '@/utils/money-format'
const props = defineProps<{
modelValue: boolean
scheduleRow: Record<string, any> | null
}>()
const emit = defineEmits(['update:modelValue'])
const innerVisible = computed({
get: () => props.modelValue,
set: (v: boolean) => emit('update:modelValue', v)
})
const loading = ref(false)
const viewData = ref<Record<string, any>>({})
const infusionMethodOptions = ref<Api.Dict.DictDataOption[]>([])
const formatInfusionMethod = (code: unknown) => {
if (!code) return undefined
const hit = infusionMethodOptions.value.find((o) => o.value === code)
return hit?.label || String(code)
}
const sections = computed((): ViewSection[] => {
const row = viewData.value
if (!row?.id) return []
return [
{
title: '申请信息',
fields: [
{ label: '申请单号', value: row.applicationNo },
{ label: '渠道名称', value: row.channelName }
]
},
{
title: '患者信息',
fields: [
{ label: '患者姓名', value: row.patientName },
{ label: '性别', value: sexLabel(row.sex) || '—' },
{ label: '年龄', value: row.age != null ? String(row.age) : undefined }
]
},
{
title: '输注计划',
fields: [
{ label: '规格', value: row.specText },
{ label: '输注时间', value: row.infusionTime },
{ label: '输注地点', value: row.infusionLocation },
{ label: '输注方式', value: formatInfusionMethod(row.infusionMethod) },
{
label: '生产状态',
value: scheduleStatusLabel(row.scheduleStatus),
tagType: scheduleStatusTagType(row.scheduleStatus) as TagType
},
{
label: '输注状态',
value: infusionStatusLabel(row.infusionStatus),
tagType: infusionStatusTagType(row.infusionStatus) as TagType
},
{
label: '质检状态',
value: qcStatusLabel(row.qcStatus),
tagType: qcStatusTagType(row.qcStatus) as TagType
}
]
},
{
title: '产品列表',
fields: [],
slot: 'products'
},
{
title: '其他信息',
fields: [{ label: '备注', value: row.remark || undefined, span: true }]
}
]
})
const loadInfusionMethodOptions = async () => {
try {
infusionMethodOptions.value = (await fetchDictDataOptions('szfs')) || []
} catch {
infusionMethodOptions.value = []
}
}
const loadDetail = async () => {
const id = props.scheduleRow?.id
if (!id) return
viewData.value = { ...(props.scheduleRow || {}) }
loading.value = true
try {
await loadInfusionMethodOptions()
const res: any = await fetchInfusionScheduleMgmtDetail({ scheduleId: id })
const detail =
res && typeof res === 'object' && !Array.isArray(res)
? (res as Record<string, any>)
: ((res as any)?.data ?? {})
viewData.value = { ...viewData.value, ...detail }
} catch {
// keep initial data from row
} finally {
loading.value = false
}
}
watch(
() => props.modelValue,
(val) => {
if (val && props.scheduleRow?.id) {
loadDetail()
} else if (!val) {
viewData.value = {}
}
}
)
</script>
<style lang="scss" scoped>
.view-inner-table-wrap.list-body.list-wrap {
margin-top: 4px;
border: 1px solid var(--mint-border, #e2e8f0);
border-radius: var(--mint-radius, 10px);
overflow: hidden;
background: #fff;
:deep(.el-table) {
--el-table-header-bg-color: var(--mint-table-head, #f8fafc);
--el-table-border-color: var(--mint-border, #e2e8f0);
--el-table-row-hover-bg-color: var(--mint-row-hover, #f0fdf4);
--el-table-text-color: var(--mint-text, #0f172a);
--el-table-header-text-color: var(--mint-text-muted, #64748b);
font-size: 14px;
}
:deep(.el-table th.el-table__cell) {
font-size: 12px;
font-weight: 600;
color: var(--mint-text-muted, #64748b);
}
:deep(.el-table td.el-table__cell) {
font-size: 14px;
font-weight: 500;
color: var(--mint-text, #0f172a);
}
:deep(.el-empty) {
padding: 24px 0;
}
}
</style>
@@ -0,0 +1,527 @@
<!-- 输注计划管理 — 列表页 -->
<template>
<div class="page-container">
<header class="page-head">
<h1>输注计划管理</h1>
<p class="page-desc">管理中试服务患者输注计划的确认、完成与取消。</p>
</header>
<section class="panel">
<div class="panel-top">
<h2>输注计划</h2>
</div>
<!-- 筛选 -->
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="申请单号/患者姓名/渠道名称"
clearable
/>
</el-form-item>
<el-form-item label="输注状态">
<el-select
v-model="searchForm.infusionStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="待输注" value="dtz" />
<el-option label="已用户确认" value="yrqr" />
<el-option label="已取消" value="yqx" />
<el-option label="已完成" value="ywc" />
</el-select>
</el-form-item>
<el-form-item label="质检状态">
<el-select
v-model="searchForm.qcStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="待质检" value="dzc" />
<el-option label="已质检" value="yzj" />
<el-option label="已取消" value="yqx" />
</el-select>
</el-form-item>
<el-form-item label="输注时间">
<el-date-picker
v-model="searchForm.infusionTimeRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">支持申请单号、患者姓名、渠道名称模糊查询,输入即检索。</span>
<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>
</div>
</div>
<!-- 表格 -->
<div class="list-body list-wrap">
<el-table
ref="tableRef"
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
:row-class-name="infusionScheduleRowClassName"
@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="(i: number) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column
prop="applicationNo"
label="申请单号"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="patientName"
label="患者姓名"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="sex"
label="性别"
min-width="60"
align="center"
header-align="center"
>
<template #default="{ row }">{{ sexLabel(row.patientSex ?? row.sex) || '—' }}</template>
</el-table-column>
<el-table-column
prop="age"
label="年龄"
min-width="60"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.patientAge ?? row.age ?? '—' }}</template>
</el-table-column>
<el-table-column
prop="channelName"
label="渠道名称"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.channelName || '—' }}</template>
</el-table-column>
<el-table-column label="第几次" min-width="88" align="center" header-align="center">
<template #default="{ row }">{{ formatScheduleSortNo(row) }}</template>
</el-table-column>
<el-table-column label="总次数" min-width="88" align="center" header-align="center">
<template #default="{ row }">{{ formatScheduleTotalCount(row) }}</template>
</el-table-column>
<el-table-column
prop="infusionTime"
label="输注时间"
min-width="160"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.infusionTime || '—' }}</template>
</el-table-column>
<el-table-column
prop="infusionLocation"
label="输注地点"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.infusionLocation || '—' }}</template>
</el-table-column>
<el-table-column
prop="scheduleStatus"
label="生产状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="scheduleStatusTagType(row.scheduleStatus)" size="small">
{{ scheduleStatusLabel(row.scheduleStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="infusionStatus"
label="输注状态"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="infusionStatusTagType(row.infusionStatus)" size="small">
{{ infusionStatusLabel(row.infusionStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="质检状态" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="qcStatusTagType(row.qcStatus)" size="small">{{
qcStatusLabel(row.qcStatus)
}}</el-tag>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="420"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button v-if="canEditSchedule(row)" class="btn-op-item" @click="handleEdit(row)">
<el-icon><EditPen /></el-icon>编辑
</el-button>
<el-button
v-if="getInfusionStatus(row) === 'dtz'"
class="btn-op-item"
@click="handleConfirm(row)"
>
<el-icon><CircleCheck /></el-icon>用户已确认
</el-button>
<el-button
v-if="canCompleteSchedule(row)"
class="btn-op-item primary"
@click="handleComplete(row)"
>
<el-icon><SuccessFilled /></el-icon>完成
</el-button>
<el-button v-if="canEditSchedule(row)" class="btn-op-item" @click="handleCancel(row)">
<el-icon><CircleClose /></el-icon>取消
</el-button>
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="
(v: number) => ((pagination.page = 1), (pagination.pageSize = v), loadData())
"
@current-change="(v: number) => ((pagination.page = v), loadData())"
/>
</div>
</div>
</section>
<!-- 编辑弹窗 -->
<ScheduleEditDialog v-model="editDialogVisible" :schedule-row="editRow" @success="loadData" />
<!-- 查看弹窗 -->
<ScheduleViewDialog v-model="viewDialogVisible" :schedule-row="viewRow" />
<!-- 取消弹窗 -->
<ScheduleCancelDialog
v-model="cancelDialogVisible"
:schedule-id="cancelScheduleId"
:patient-name="cancelPatientName"
@success="loadData"
/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
RefreshLeft,
Search,
View,
EditPen,
CircleCheck,
CircleClose,
SuccessFilled
} from '@element-plus/icons-vue'
import { fetchInfusionScheduleMgmtList, updateInfusionScheduleStatus } from '@/api/admin'
import {
scheduleStatusLabel,
scheduleStatusTagType,
infusionStatusLabel,
infusionStatusTagType,
qcStatusLabel,
qcStatusTagType,
channelServiceOrderTypeLabel,
sexLabel
} from '@/utils/status-tag-registry'
import ScheduleEditDialog from './ScheduleEditDialog.vue'
import ScheduleViewDialog from './ScheduleViewDialog.vue'
import ScheduleCancelDialog from './ScheduleCancelDialog.vue'
defineOptions({ name: 'InfusionScheduleManagement' })
// ── 搜索(输注时间默认最近 30 天) ──
const getDefaultTimeRange = () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 30 * 24 * 3600 * 1000)
const fmt = (d: Date) => d.toISOString().slice(0, 10)
return [fmt(start), fmt(end)]
}
const searchForm = reactive({
keyword: '',
infusionStatus: '',
qcStatus: '',
infusionTimeRange: getDefaultTimeRange() as string[]
})
// ── 表格 ─────────────────────────────
const loading = ref(false)
const tableData = ref<any[]>([])
const tableRef = ref()
const selectedRows = ref<any[]>([])
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const handleSelectionChange = (rows: any[]) => {
selectedRows.value = rows
}
/** 输注时间落在本周(周一至周日) */
const isInfusionThisWeek = (infusionTime?: string | null): boolean => {
if (!infusionTime) return false
const dateStr = String(infusionTime).trim().slice(0, 10)
const infusionDate = new Date(`${dateStr}T00:00:00`)
if (Number.isNaN(infusionDate.getTime())) return false
const today = new Date()
today.setHours(0, 0, 0, 0)
const weekday = today.getDay()
const diffToMonday = weekday === 0 ? -6 : 1 - weekday
const weekStart = new Date(today)
weekStart.setDate(today.getDate() + diffToMonday)
const weekEnd = new Date(weekStart)
weekEnd.setDate(weekStart.getDate() + 6)
weekEnd.setHours(23, 59, 59, 999)
return infusionDate >= weekStart && infusionDate <= weekEnd
}
const infusionScheduleRowClassName = ({ row }: { row: Record<string, unknown> }) => {
return isInfusionThisWeek(row.infusionTime as string) ? 'row-infusion-this-week' : ''
}
const formatScheduleSortNo = (row: Record<string, unknown>) => {
const n = row.sortNo ?? row.orderNum
if (n == null || n === '' || Number(n) <= 0) return '—'
return `第${Number(n)}次`
}
const formatScheduleTotalCount = (row: Record<string, unknown>) => {
const n = row.totalCourseCount
if (n == null || n === '' || Number(n) <= 0) return '—'
return `共${Number(n)}次`
}
// ── 弹窗 ─────────────────────────────
const editDialogVisible = ref(false)
const editRow = ref<any>(null)
const viewDialogVisible = ref(false)
const viewRow = ref<any>(null)
const cancelDialogVisible = ref(false)
const cancelScheduleId = ref('')
const cancelPatientName = ref('')
// ── 数据加载 ──────────────────────────
const loadData = async () => {
loading.value = true
try {
const params: any = {
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
infusionStatus: searchForm.infusionStatus || undefined,
qcStatus: searchForm.qcStatus || undefined
}
if (searchForm.infusionTimeRange?.length === 2) {
params.infusionTimeStart = searchForm.infusionTimeRange[0]
params.infusionTimeEnd = searchForm.infusionTimeRange[1]
}
const res: any = await fetchInfusionScheduleMgmtList(params)
const data = res?.data ?? res
tableData.value = data?.list ?? data?.records ?? []
pagination.total = data?.pagination?.total ?? data?.total ?? 0
} catch (e) {
console.error('加载输注计划列表失败:', e)
tableData.value = []
pagination.total = 0
} finally {
loading.value = false
}
}
// ── 事件 ──────────────────────────────
const handleSearch = () => {
pagination.page = 1
loadData()
}
const handleReset = () => {
Object.assign(searchForm, {
keyword: '',
infusionStatus: '',
qcStatus: '',
infusionTimeRange: getDefaultTimeRange()
})
pagination.page = 1
loadData()
}
const handleEdit = (row: any) => {
editRow.value = { ...row }
editDialogVisible.value = true
}
const handleView = (row: any) => {
viewRow.value = { ...row }
viewDialogVisible.value = true
}
const getInfusionStatus = (row: any) =>
String(row?.infusionStatus || row?.infusion_status || '').trim()
const canEditSchedule = (row: any) => {
const s = getInfusionStatus(row)
return s === 'dtz' || s === 'yrqr'
}
/** 待输注、已用户确认均可完成(待输注时将自动先确认再完成) */
const canCompleteSchedule = (row: any) => canEditSchedule(row)
const buildCompleteConfirmMessage = (row: any) => {
const orderTypeLabel = channelServiceOrderTypeLabel(row.orderType)
const patient = row.patientName ? `患者「${row.patientName}」` : '该患者'
const statusHint =
getInfusionStatus(row) === 'dtz' ? '当前为待输注,系统将先记录用户确认再完成结算。' : ''
let settlementHint = '并触发关联服务订单的结算扣减。'
if (row.orderType === 'yfk') {
settlementHint = `关联服务订单类型为「${orderTypeLabel}」,系统将按渠道协议单价扣减服务订单账户余额。`
} else if (row.orderType === 'gmcp') {
settlementHint = `关联服务订单类型为「${orderTypeLabel}」,系统将按份数扣减服务订单中的细胞产品持仓。`
}
return `${patient}的输注计划将标记为已完成。${statusHint}${settlementHint}是否继续?`
}
const handleConfirm = async (row: any) => {
try {
await ElMessageBox.confirm('确认该输注计划已由用户确认?', '用户已确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info'
})
await updateInfusionScheduleStatus({
scheduleId: row.id,
infusionStatus: 'yrqr'
})
ElMessage.success('已确认用户确认')
loadData()
} catch (e: any) {
if (e !== 'cancel') {
// error handled by request util
}
}
}
const handleComplete = async (row: any) => {
try {
await ElMessageBox.confirm(buildCompleteConfirmMessage(row), '完成输注计划', {
confirmButtonText: '确定完成',
cancelButtonText: '取消',
type: 'warning'
})
const scheduleId = row.id
if (getInfusionStatus(row) === 'dtz') {
await updateInfusionScheduleStatus({
scheduleId,
infusionStatus: 'yrqr'
})
}
await updateInfusionScheduleStatus({
scheduleId,
infusionStatus: 'ywc'
})
ElMessage.success('输注计划已完成,结算扣减已处理')
loadData()
} catch (e: any) {
if (e !== 'cancel') {
// error handled by request util
}
}
}
const handleCancel = (row: any) => {
cancelScheduleId.value = row.id
cancelPatientName.value = row.patientName || ''
cancelDialogVisible.value = true
}
watchDebounced(
searchForm,
() => {
pagination.page = 1
loadData()
},
{ debounce: 300, deep: true }
)
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.list-table :deep(tr.row-infusion-this-week > td.el-table__cell) {
background-color: #fee2e2 !important;
color: #991b1b;
}
.list-table :deep(tr.row-infusion-this-week:hover > td.el-table__cell) {
background-color: #fecaca !important;
}
</style>
@@ -24,7 +24,6 @@
{{ applicationStatusLabel(applicationDetail.applicationStatus) }}
</el-tag>
</div>
</div>
</template>
@@ -32,7 +31,7 @@
<!-- detail-tabs sticky 导航 -->
<div class="detail-tabs">
<div
v-for="tab in (isCreateMode ? createDetailTabs : detailTabs)"
v-for="tab in isCreateMode ? createDetailTabs : detailTabs"
:key="tab.key"
class="detail-tab"
:class="{ active: activeTab === tab.key }"
@@ -222,7 +221,7 @@
</el-form-item>
</div>
</el-form>
</div>
</div>
</div>
<!-- Tab 2: 输注方案(创建模式本地暂存) -->
@@ -784,7 +783,11 @@
<div class="attachment-footer">
<span class="attachment-name" :title="file.name">{{ file.name }}</span>
<div class="attachment-actions">
<el-icon class="action-btn" title="下载" @click.stop="downloadFile(file)">
<el-icon
class="action-btn"
title="下载"
@click.stop="downloadFile(file)"
>
<Download />
</el-icon>
</div>
@@ -59,7 +59,11 @@
/>
</el-select>
</el-form-item>
<el-form-item label="服务订单" prop="channelServiceOrderId" class="form-col-span-2-cols">
<el-form-item
label="服务订单"
prop="channelServiceOrderId"
class="form-col-span-2-cols"
>
<div class="field-with-action">
<el-select
v-model="formData.channelServiceOrderId"
@@ -227,7 +231,9 @@
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
<span class="attachment-name" :title="file.fileName">{{
file.fileName
}}</span>
<div class="attachment-actions">
<el-icon class="action-btn" @click.stop="downloadFile(file)">
<Download />
@@ -305,10 +311,7 @@
</el-form-item>
<el-form-item>
<template #label>疗程间隔<span class="form-label-tip">(月)</span></template>
<el-input
v-model="infusionPlan.courseInterval"
placeholder="请输入疗程间隔"
/>
<el-input v-model="infusionPlan.courseInterval" placeholder="请输入疗程间隔" />
</el-form-item>
<el-form-item label="输注开始日期">
<el-date-picker
@@ -353,7 +356,24 @@
<div class="schedule-header" @click="toggleScheduleExpand(sIdx)">
<span class="schedule-chevron">▶</span>
<span class="schedule-title">输注计划 #{{ sIdx + 1 }}</span>
<el-tag
v-if="schedule.infusionStatus"
:type="infusionStatusTagType(normalizeInfusionStatusCode(schedule.infusionStatus))"
size="small"
class="schedule-status-tag"
>
{{ infusionStatusLabel(normalizeInfusionStatusCode(schedule.infusionStatus)) }}
</el-tag>
<el-tag
v-if="schedule.scheduleStatus && isEdit"
:type="scheduleStatusTagType(schedule.scheduleStatus)"
size="small"
class="schedule-status-tag"
>
{{ scheduleStatusLabel(schedule.scheduleStatus) }}
</el-tag>
<el-button
v-if="canDeleteSchedule(schedule)"
link
type="danger"
size="small"
@@ -373,6 +393,7 @@
value-format="YYYY-MM-DD"
placeholder="自动计算,可修改"
clearable
:disabled="!isScheduleEditable(schedule)"
style="width: 100%"
/>
</el-form-item>
@@ -380,6 +401,7 @@
<el-input
v-model="schedule.infusionLocation"
placeholder="请输入输注地点"
:disabled="!isScheduleEditable(schedule)"
/>
</el-form-item>
<el-form-item label="输注方式">
@@ -388,6 +410,7 @@
clearable
filterable
placeholder="请选择输注方式"
:disabled="!isScheduleEditable(schedule)"
style="width: 100%"
>
<el-option
@@ -407,7 +430,7 @@
<el-button
type="primary"
size="small"
:disabled="!canAddCellProduct"
:disabled="!canAddCellProduct || !isScheduleEditable(schedule)"
@click="addCellProduct(sIdx)"
>
<el-icon><Plus /></el-icon>
@@ -428,10 +451,8 @@
clearable
placeholder="选择项目"
class="cell-product-table__control"
:disabled="!canAddCellProduct"
@change="
onCellProductSelect(sIdx, pIdx, $event)
"
:disabled="!canAddCellProduct || !isScheduleEditable(schedule)"
@change="onCellProductSelect(sIdx, pIdx, $event)"
>
<el-option
v-for="p in getCellProductPriceOptions(sIdx, pIdx)"
@@ -442,16 +463,8 @@
</el-select>
</template>
</el-table-column>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="100"
/>
<el-table-column
prop="specText"
label="规格"
min-width="100"
/>
<el-table-column prop="cellTypeName" label="细胞类型" min-width="100" />
<el-table-column prop="specText" label="规格" min-width="100" />
<el-table-column label="协议单价(元)" min-width="130" align="right">
<template #default="{ row }">
<span class="cell-product-price-text">
@@ -465,6 +478,7 @@
v-model="row.actualPrice"
:min="0"
:precision="2"
:disabled="!isScheduleEditable(schedule)"
controls-position="right"
class="cell-product-table__control"
/>
@@ -475,6 +489,7 @@
<el-input-number
v-model="row.quantity"
:min="1"
:disabled="!isScheduleEditable(schedule)"
controls-position="right"
class="cell-product-table__control"
/>
@@ -485,18 +500,27 @@
<el-input
v-model="row.remark"
placeholder="备注"
:disabled="!isScheduleEditable(schedule)"
class="cell-product-table__control"
/>
</template>
</el-table-column>
<el-table-column label="状态" min-width="80" align="center">
<template #default="{ row }">
<el-tag size="small" type="info">{{ row.status }}</el-tag>
<el-tag
:type="infusionStatusTagType(normalizeInfusionStatusCode(row.status))"
size="small"
>
{{
infusionStatusLabel(normalizeInfusionStatusCode(row.status))
}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="60" align="center">
<template #default="{ $index: pIdx }">
<el-button
v-if="isScheduleEditable(schedule)"
link
type="danger"
size="small"
@@ -622,15 +646,45 @@
class="list-table inner-table"
max-height="360"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="150" show-overflow-tooltip />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="150"
show-overflow-tooltip
/>
<el-table-column prop="cellType" label="细胞类型" min-width="120" show-overflow-tooltip />
<el-table-column prop="specText" label="规格文本" min-width="150" show-overflow-tooltip />
<el-table-column label="协议单价(元)" min-width="130" align="right" header-align="center">
<template #default="{ row }">¥{{ formatOrderBalance(row.agreementUnitPrice) }}</template>
<el-table-column
label="协议单价(元)"
min-width="130"
align="right"
header-align="center"
>
<template #default="{ row }"
>¥{{ formatOrderBalance(row.agreementUnitPrice) }}</template
>
</el-table-column>
<el-table-column prop="currentQty" label="当前持有" min-width="100" align="center" header-align="center" />
<el-table-column prop="totalBuyQty" label="累计购买" min-width="100" align="center" header-align="center" />
<el-table-column
prop="currentQty"
label="当前持有"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="totalBuyQty"
label="累计购买"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="totalUnsubscribeQty"
label="累计退订"
@@ -668,12 +722,38 @@
class="list-table inner-table"
max-height="360"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="projectCode" label="项目编码" min-width="110" show-overflow-tooltip />
<el-table-column prop="cellTypeName" label="细胞类型" min-width="110" show-overflow-tooltip />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
prop="projectCode"
label="项目编码"
min-width="110"
show-overflow-tooltip
/>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="110"
show-overflow-tooltip
/>
<el-table-column prop="specText" label="规格" min-width="120" show-overflow-tooltip />
<el-table-column label="协议单价(元)" min-width="130" align="right" header-align="center">
<el-table-column
label="协议单价(元)"
min-width="130"
align="right"
header-align="center"
>
<template #default="{ row }">¥{{ formatOrderBalance(row.unitPrice) }}</template>
</el-table-column>
</el-table>
@@ -685,7 +765,17 @@
<script setup lang="ts">
import type { FormInstance } from 'element-plus'
import { Plus, Delete, Close, Check, Download, Document, VideoPlay, Loading, View } from '@element-plus/icons-vue'
import {
Plus,
Delete,
Close,
Check,
Download,
Document,
VideoPlay,
Loading,
View
} from '@element-plus/icons-vue'
import {
savePilotApplication,
fetchPilotApplicationDetail,
@@ -702,7 +792,9 @@
import { fetchUploadAttachment } from '@/api/business'
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
import DialogFormBody from '@/components/business/dialog-form-body/index.vue'
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
import SystemViewDialog, {
type ViewSection
} from '@/components/business/system-view-dialog/index.vue'
import ServiceOrderTxnTable from '@/views/pilot-service/channel-service-order/ServiceOrderTxnTable.vue'
import {
channelServiceOrderTypeLabel,
@@ -714,7 +806,11 @@
agreementStatusLabel,
agreementStatusTagType,
settlementTypeLabel,
settlementTypeTagType
settlementTypeTagType,
infusionStatusLabel,
infusionStatusTagType,
scheduleStatusLabel,
scheduleStatusTagType
} from '@/utils/status-tag-registry'
import { formatMoneyDisplay } from '@/utils/money-format'
@@ -1250,7 +1346,11 @@
const remoteSearchCustomers = async (keyword: string) => {
customerLoading.value = true
try {
const res: any = await fetchCustomerList({ page: 1, pageSize: 50, keyword: keyword || undefined })
const res: any = await fetchCustomerList({
page: 1,
pageSize: 50,
keyword: keyword || undefined
})
const data = res?.data ?? res
customerOptions.value = data?.list ?? data?.records ?? []
} catch {
@@ -1381,7 +1481,7 @@
fileSize: file.size,
fileSizeDisplay: `${(file.size / 1024).toFixed(1)} KB`,
fileType: file.type || '',
_uploading: true,
_uploading: true
})
const idx = diagnosisFileList.value.length - 1
@@ -1397,7 +1497,7 @@
fileSize: res.fileSize || file.size,
fileSizeDisplay: res.fileSizeDisplay || `${(file.size / 1024).toFixed(1)} KB`,
fileType: res.fileType || file.type || '',
_uploading: false,
_uploading: false
}
} catch (e: any) {
ElMessage.error(e?.message || '上传失败')
@@ -1423,12 +1523,40 @@
// ── 输注方案(inline,一个申请单一个方案) ──────────────────────────────
const infusionPlan = reactive({
id: '' as string,
totalCourseCount: 0,
courseInterval: '',
infusionStartDate: '',
channelAgreementId: ''
})
const INFUSION_STATUS_CN_TO_CODE: Record<string, string> = {
待输注: 'dtz',
已用户确认: 'yrqr',
已取消: 'yqx',
已完成: 'ywc'
}
function normalizeInfusionStatusCode(raw: unknown): string {
const s = String(raw ?? '').trim()
if (!s) return ''
return INFUSION_STATUS_CN_TO_CODE[s] || s
}
/** 待输注 / 已用户确认:可编辑时间与细胞产品 */
function isScheduleEditable(schedule: Record<string, any>): boolean {
if (!isEdit.value) return true
const st = normalizeInfusionStatusCode(schedule.infusionStatus)
return st === 'dtz' || st === 'yrqr'
}
/** 仅待输注可删疗程 */
function canDeleteSchedule(schedule: Record<string, any>): boolean {
if (!isEdit.value) return true
if (!schedule.id) return true
return normalizeInfusionStatusCode(schedule.infusionStatus) === 'dtz'
}
// 输注计划列表
let scheduleSeq = 0
const infusionSchedules = ref<any[]>([])
@@ -1574,6 +1702,11 @@
}
const removeSchedule = (idx: number) => {
const s = infusionSchedules.value[idx]
if (s && !canDeleteSchedule(s)) {
ElMessage.warning('当前疗程不可删除(仅待输注且未开始生产的计划可删)')
return
}
infusionSchedules.value.splice(idx, 1)
recalcAllScheduleTimes()
syncTotalCourseCountWithSchedules()
@@ -1583,6 +1716,10 @@
const addCellProduct = (scheduleIdx: number) => {
const s = infusionSchedules.value[scheduleIdx]
if (!s) return
if (!isScheduleEditable(s)) {
ElMessage.warning('当前疗程状态不允许修改细胞产品')
return
}
const usedKeys = new Set(
(s.cellProducts || []).map((cp: any) => getCellProductProjectKey(cp)).filter(Boolean)
)
@@ -1605,13 +1742,17 @@
actualPrice: 0,
quantity: 1,
remark: '',
status: '待输注'
status: 'dtz'
})
}
const removeCellProduct = (scheduleIdx: number, productIdx: number) => {
const s = infusionSchedules.value[scheduleIdx]
if (!s) return
if (!isScheduleEditable(s)) {
ElMessage.warning('当前疗程状态不允许修改细胞产品')
return
}
s.cellProducts.splice(productIdx, 1)
}
@@ -1701,6 +1842,7 @@
patientForm.clinicalDiagnosis = ''
patientForm.needCheckup = 0
diagnosisFileList.value = []
infusionPlan.id = ''
infusionPlan.totalCourseCount = 0
infusionPlan.courseInterval = ''
infusionPlan.infusionStartDate = ''
@@ -1775,7 +1917,7 @@
fileSize: 0,
fileSizeDisplay: '',
fileType: '',
_uploading: false,
_uploading: false
}
})
}
@@ -1788,6 +1930,7 @@
try {
if (detail.infusionPlan) {
const ip = detail.infusionPlan
infusionPlan.id = ip.id || ''
infusionPlan.totalCourseCount = ip.totalCourseCount || 0
infusionPlan.courseInterval = ip.courseInterval || ''
infusionPlan.infusionStartDate = ip.infusionStartDate || ''
@@ -1795,12 +1938,16 @@
}
if (detail.infusionSchedules && Array.isArray(detail.infusionSchedules)) {
infusionSchedules.value = detail.infusionSchedules.map((s: any, i: number) => ({
id: s.id || '',
_localId: ++scheduleSeq,
infusionTime: s.infusionTime || '',
infusionLocation: s.infusionLocation || '',
infusionMethod: s.infusionMethod || '',
infusionStatus: normalizeInfusionStatusCode(s.infusionStatus) || 'dtz',
scheduleStatus: s.scheduleStatus || 'dsc',
_expanded: i === 0,
cellProducts: (s.cellProducts || []).map((cp: any) => ({
id: cp.id || '',
_localId: ++scheduleSeq,
priceId: cp.priceId || '',
projectCode: cp.projectCode || '',
@@ -1811,7 +1958,7 @@
actualPrice: cp.actualPrice ?? 0,
quantity: cp.quantity || 1,
remark: cp.remark || '',
status: cp.status || '待输注'
status: normalizeInfusionStatusCode(cp.status) || 'dtz'
}))
}))
}
@@ -1856,21 +2003,28 @@
emergencyContact: patientForm.emergencyContact || undefined,
emergencyPhone: patientForm.emergencyPhone || undefined,
clinicalDiagnosis: patientForm.clinicalDiagnosis || undefined,
diagnosisAttachments: diagnosisFileList.value.map((f) => f.fileUrl).filter(Boolean).join(',') || undefined,
diagnosisAttachments:
diagnosisFileList.value
.map((f) => f.fileUrl)
.filter(Boolean)
.join(',') || undefined,
needCheckup: patientForm.needCheckup
},
infusionPlan: {
id: infusionPlan.id || undefined,
totalCourseCount: infusionPlan.totalCourseCount,
courseInterval: infusionPlan.courseInterval || undefined,
infusionStartDate: infusionPlan.infusionStartDate || undefined,
channelAgreementId: infusionPlan.channelAgreementId || undefined
},
infusionSchedules: infusionSchedules.value.map((s, idx) => ({
id: s.id || undefined,
sortNo: idx + 1,
infusionTime: s.infusionTime || undefined,
infusionLocation: s.infusionLocation || undefined,
infusionMethod: s.infusionMethod || undefined,
cellProducts: (s.cellProducts || []).map((cp: any) => ({
id: cp.id || undefined,
priceId: cp.priceId || undefined,
projectCode: cp.projectCode || undefined,
projectName: cp.projectName || undefined,
@@ -1879,8 +2033,7 @@
agreementPrice: cp.agreementPrice ?? undefined,
actualPrice: cp.actualPrice ?? undefined,
quantity: cp.quantity || 1,
remark: cp.remark || undefined,
status: cp.status || '待输注'
remark: cp.remark || undefined
}))
}))
}
@@ -2370,6 +2523,10 @@
color: #1e293b;
}
.schedule-status-tag {
margin-left: 8px;
}
.schedule-delete-btn {
margin-left: auto;
}
File diff suppressed because it is too large. Load diff
@@ -162,12 +162,12 @@
<template #default="{ row }">{{ row.patientName || '-' }}</template>
</el-table-column>
<el-table-column
prop="infusionPlanCount"
label="输注方案数"
prop="infusionScheduleCount"
label="输注计划数"
min-width="100"
align="center"
>
<template #default="{ row }">{{ row.infusionPlanCount ?? 0 }}</template>
<template #default="{ row }">{{ row.infusionScheduleCount ?? 0 }}</template>
</el-table-column>
<el-table-column prop="applicationStatus" label="状态" min-width="100" align="center">
<template #default="{ row }">
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按体系名称、编码模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -82,14 +86,26 @@
align="center"
header-align="center"
/>
<el-table-column prop="status" label="状态" min-width="80" align="center" header-align="center">
<el-table-column
prop="status"
label="状态"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="String(row.status) === '1' ? 'success' : 'danger'" size="small">
{{ String(row.status) === '1' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" 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="创建时间"
@@ -97,7 +113,13 @@
align="center"
header-align="center"
/>
<el-table-column label="操作" min-width="320" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -166,24 +188,55 @@
<div class="detail-section">
<div class="detail-section-head">
<h3 class="detail-section-title">价格明细</h3>
<el-button class="btn-main" type="primary" size="small" @click="handleOpenPriceSelector">
<el-button
class="btn-main"
type="primary"
size="small"
@click="handleOpenPriceSelector"
>
<el-icon><Plus /></el-icon>
从价格体系添加
</el-button>
</div>
<el-table :data="detailItems" size="small" class="detail-table" max-height="320">
<el-table-column type="index" label="序号" min-width="55" align="center" header-align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="120" align="center" header-align="center" />
<el-table-column prop="projectCode" label="项目编码" min-width="100" align="center" header-align="center" />
<el-table-column prop="cellTypeName" label="细胞类型" min-width="100" align="center" header-align="center" />
<el-table-column prop="specText" label="规格" min-width="100" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="55"
align="center"
header-align="center"
/>
<el-table-column
prop="projectName"
label="项目名称"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="projectCode"
label="项目编码"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="specText"
label="规格"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column label="单价(元)" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<AmountInput
v-model="row.unitPrice"
:min="0"
placeholder="请输入单价"
/>
<AmountInput v-model="row.unitPrice" :min="0" placeholder="请输入单价" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="140" align="center" header-align="center">
@@ -191,9 +244,21 @@
<el-input v-model="row.remark" placeholder="备注" clearable />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="80"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ $index }">
<el-button class="btn-op-item" type="danger" link size="small" @click="detailItems.splice($index, 1)">
<el-button
class="btn-op-item"
type="danger"
link
size="small"
@click="detailItems.splice($index, 1)"
>
<el-icon><Delete /></el-icon>删除
</el-button>
</template>
@@ -239,16 +304,46 @@
@selection-change="handlePriceSelectorChange"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column prop="projectName" label="项目名称" min-width="120" align="center" header-align="center" />
<el-table-column prop="projectCode" label="项目编码" min-width="100" align="center" header-align="center" />
<el-table-column prop="cellTypeName" label="细胞类型" min-width="100" align="center" header-align="center" />
<el-table-column prop="specText" label="规格" min-width="100" align="center" header-align="center" />
<el-table-column
prop="projectName"
label="项目名称"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="projectCode"
label="项目编码"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="cellTypeName"
label="细胞类型"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="specText"
label="规格"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column label="单价" min-width="80" 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="120" align="center" header-align="center" />
<el-table-column
prop="remark"
label="备注"
min-width="120"
align="center"
header-align="center"
/>
</el-table>
<template #footer>
<el-button class="btn-ghost" @click="priceSelectorVisible = false">取消</el-button>
@@ -278,11 +373,21 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Edit, Delete, ScaleToOriginal, RefreshLeft, Search } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import AmountInput from '@/components/common/amount-input/index.vue'
import { formatMoneyDisplay } from '@/utils/money-format'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
Plus,
Close,
Check,
View,
Edit,
Delete,
ScaleToOriginal,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import AmountInput from '@/components/common/amount-input/index.vue'
import { formatMoneyDisplay } from '@/utils/money-format'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchProjectSystemList,
fetchCreateProjectSystem,
@@ -461,7 +566,9 @@ import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen
// load existing detail items
try {
const res = await fetchProjectSystemItems(row.id)
detailItems.value = ((res as any)?.list || (res as any) || []).map((item: any) => ({ ...item }))
detailItems.value = ((res as any)?.list || (res as any) || []).map((item: any) => ({
...item
}))
} catch {
detailItems.value = []
}
@@ -545,7 +652,9 @@ import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen
// --- Delete ---
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm('确定要删除该体系吗?删除后关联的价格明细也将清除。', '提示', { type: 'warning' })
await ElMessageBox.confirm('确定要删除该体系吗?删除后关联的价格明细也将清除。', '提示', {
type: 'warning'
})
} catch {
return
}
@@ -560,7 +669,9 @@ import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条记录吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条记录吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteProjectSystem(id)
@@ -48,7 +48,14 @@
<script setup lang="ts">
import { ref, watch, nextTick, computed, provide } from 'vue'
import { ElMessage } from 'element-plus'
import { VueFlow, useVueFlow, type Edge, type Node, type NodeMouseEvent, type EdgeMouseEvent } from '@vue-flow/core'
import {
VueFlow,
useVueFlow,
type Edge,
type Node,
type NodeMouseEvent,
type EdgeMouseEvent
} from '@vue-flow/core'
import '@vue-flow/core/dist/style.css'
import '@vue-flow/core/dist/theme-default.css'
@@ -58,11 +65,16 @@
import ChainNodeCard from './source-chain/ChainNodeCard.vue'
import ChainLinkEdge from './source-chain/ChainLinkEdge.vue'
import SampleCustomerViewDialog from './source-chain/SampleCustomerViewDialog.vue'
import type { ChainApiData, ChainEdgeData, ChainNode, ChainNodeData } from './source-chain/chain-types'
import type {
ChainApiData,
ChainEdgeData,
ChainNode,
ChainNodeData
} from './source-chain/chain-types'
import {
chainApiToElements,
hasChainGraphContent,
layoutChainElements,
layoutChainElements
} from './source-chain/chainToFlow'
import { chainDefaultEdgeOptions } from './source-chain/chainFlowTheme'
import { CHAIN_OPEN_TASK_KEY } from './source-chain/chainInject'
@@ -87,9 +99,7 @@
const sampleViewVisible = ref(false)
const sampleViewNode = ref<ChainNode | null>(null)
const showFlow = computed(
() => chainData.value && hasChainGraphContent(chainData.value)
)
const showFlow = computed(() => chainData.value && hasChainGraphContent(chainData.value))
const defaultEdgeOptions = chainDefaultEdgeOptions
@@ -149,9 +159,7 @@
async function openSampleCustomer(chainNode: ChainNode) {
let node = chainNode
const c = chainNode.customer
const hasCustomer =
c &&
(c.motherName || c.motherPhone || c.motherIdCard || c.newbornName)
const hasCustomer = c && (c.motherName || c.motherPhone || c.motherIdCard || c.newbornName)
if (!hasCustomer && chainNode.collectionProtocolId) {
try {
@@ -167,8 +175,8 @@
motherName: pc.motherName || '',
motherPhone: pc.motherPhone || '',
motherIdCard: pc.motherIdCard || '',
newbornName: pc.newbornName || '',
},
newbornName: pc.newbornName || ''
}
}
}
} catch {
@@ -217,7 +225,7 @@
<script lang="ts">
export default {
name: 'SourceChainGraph',
name: 'SourceChainGraph'
}
</script>
@@ -44,12 +44,7 @@
clearable
style="width: 100%"
>
<el-option
v-for="p in passageOptions"
:key="p"
:label="p"
:value="p"
/>
<el-option v-for="p in passageOptions" :key="p" :label="p" :value="p" />
</el-select>
</el-form-item>
<el-form-item label="来源类型">
@@ -108,12 +103,7 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column
type="selection"
min-width="55"
align="center"
header-align="center"
/>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -122,60 +112,25 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column
prop="productCode"
label="产品编码"
min-width="180"
align="center"
/>
<el-table-column
prop="productName"
label="产品名称"
min-width="160"
align="center"
/>
<el-table-column
prop="productTypeName"
label="产品类型"
min-width="120"
align="center"
>
<el-table-column prop="productCode" label="产品编码" min-width="180" align="center" />
<el-table-column prop="productName" label="产品名称" min-width="160" align="center" />
<el-table-column prop="productTypeName" label="产品类型" min-width="120" align="center">
<template #default="{ row }">
{{ row.productTypeName || '—' }}
</template>
</el-table-column>
<el-table-column
prop="specName"
label="规格"
min-width="120"
align="center"
>
<el-table-column prop="specName" label="规格" min-width="120" align="center">
<template #default="{ row }">
{{ row.specName || '—' }}
</template>
</el-table-column>
<el-table-column
prop="passage"
label="代次"
min-width="80"
align="center"
>
<el-table-column prop="passage" label="代次" min-width="80" align="center">
<template #default="{ row }">
{{ row.passage || '—' }}
</template>
</el-table-column>
<el-table-column
prop="batchNo"
label="批次号"
min-width="140"
align="center"
/>
<el-table-column
prop="storageSlotName"
label="储物格位置"
min-width="140"
align="center"
>
<el-table-column prop="batchNo" label="批次号" min-width="140" align="center" />
<el-table-column prop="storageSlotName" label="储物格位置" min-width="140" align="center">
<template #default="{ row }">
{{ row.storageSlotName || '—' }}
</template>
@@ -190,27 +145,14 @@
{{ row.expectedCellCount ?? '—' }}
</template>
</el-table-column>
<el-table-column
prop="viability"
label="活性(%)"
min-width="90"
align="center"
>
<el-table-column prop="viability" label="活性(%)" min-width="90" align="center">
<template #default="{ row }">
{{ row.viability != null ? row.viability : '—' }}
</template>
</el-table-column>
<el-table-column
prop="productStatus"
label="状态"
min-width="100"
align="center"
>
<el-table-column prop="productStatus" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag
:type="productStatusTag(row.productStatus).type"
size="small"
>
<el-tag :type="productStatusTag(row.productStatus).type" size="small">
{{ productStatusTag(row.productStatus).label }}
</el-tag>
</template>
@@ -227,56 +169,27 @@
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
label="来源协议号"
min-width="160"
align="center"
show-overflow-tooltip
>
<el-table-column label="来源协议号" min-width="160" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.sourceProtocolNo || '—' }}</template>
</el-table-column>
<el-table-column
label="来源工单号"
min-width="140"
align="center"
show-overflow-tooltip
>
<el-table-column label="来源工单号" min-width="140" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.sourceWorkOrderNo || '—' }}</template>
</el-table-column>
<el-table-column
label="来源任务号"
min-width="140"
align="center"
show-overflow-tooltip
>
<el-table-column label="来源任务号" min-width="140" align="center" show-overflow-tooltip>
<template #default="{ row }">{{ row.sourceTaskNo || '—' }}</template>
</el-table-column>
<el-table-column
prop="expiryDate"
label="有效期"
min-width="120"
align="center"
>
<el-table-column prop="expiryDate" label="有效期" min-width="120" align="center">
<template #default="{ row }">
{{ row.expiryDate || '—' }}
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
>
<el-table-column label="操作" min-width="260" fixed="right" align="center">
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-button class="btn-op-item primary" @click="handleEdit(row)">
编辑
</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">
删除
</el-button>
<el-button class="btn-op-item primary" @click="handleEdit(row)"> 编辑 </el-button>
<el-button class="btn-op-item" @click="handleDelete(row)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
@@ -361,12 +274,7 @@
clearable
style="width: 100%"
>
<el-option
v-for="p in passageOptions"
:key="p"
:label="p"
:value="p"
/>
<el-option v-for="p in passageOptions" :key="p" :label="p" :value="p" />
</el-select>
</el-form-item>
<el-form-item label="批次号" prop="batchNo">
@@ -415,10 +323,7 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item
:label="isEdit ? '数量' : '入库份数'"
prop="quantity"
>
<el-form-item :label="isEdit ? '数量' : '入库份数'" prop="quantity">
<el-input-number
v-model="formData.quantity"
:min="isEdit ? 0 : 1"
@@ -537,12 +442,7 @@
</div>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="可选"
/>
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
<div v-if="!isEdit" class="product-lines-section">
<div class="product-lines-head">
@@ -552,12 +452,7 @@
>
</div>
<el-table :data="productLines" size="small" class="product-lines-table">
<el-table-column
type="index"
label="序号"
min-width="64"
align="center"
/>
<el-table-column type="index" label="序号" min-width="64" align="center" />
<el-table-column label="产品编码" min-width="200">
<template #default="{ row }">
<el-input
@@ -594,12 +489,7 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button
class="btn-main"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit">
<el-icon><Check /></el-icon>
确定
</el-button>
@@ -686,7 +576,7 @@
Box,
Delete,
Download,
Loading,
Loading
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
@@ -698,14 +588,14 @@
fetchCreateCellProduct,
fetchUpdateCellProduct,
fetchDeleteCellProduct,
fetchNextCellProductCode,
fetchNextCellProductCode
} from '@/api/cell-product'
import {
fetchCellProductTypeList,
fetchCellProductSpecList,
fetchStorageFacilityList,
fetchStorageSlotList,
fetchItemUnitList,
fetchItemUnitList
} from '@/api/admin'
import { loadStorageSlotSelectOptions } from '@/utils/storage-slot-options'
import { fetchUploadAttachment } from '@/api/business'
@@ -742,7 +632,7 @@
sold: { label: '已售卖', type: 'warning' },
used: { label: '已使用', type: 'info' },
voided: { label: '已作废', type: 'danger' },
thawed: { label: '已复苏', type: 'info' },
thawed: { label: '已复苏', type: 'info' }
}
return map[status] || { label: status, type: 'info' }
}
@@ -788,7 +678,7 @@
{ label: '已复苏', value: 'thawed' },
{ label: '已售卖', value: 'sold' },
{ label: '已使用', value: 'used' },
{ label: '已作废', value: 'voided' },
{ label: '已作废', value: 'voided' }
]
function mapUnitOption(u: { unitName?: string; unitCode?: string }) {
@@ -796,7 +686,7 @@
const code = (u.unitCode || '').trim()
return {
label: code ? `${name}(${code})` : name,
value: name,
value: name
}
}
@@ -814,7 +704,7 @@
const res = await fetchItemUnitList({
page: 1,
pageSize: 100,
keyword: query?.trim() || undefined,
keyword: query?.trim() || undefined
})
unitOptions.value = (res.list || []).map(mapUnitOption).filter((o) => o.value)
} catch {
@@ -841,7 +731,7 @@
expiryDate: '',
productStatus: 'frozen',
productImages: '',
remark: '',
remark: ''
})
const productLines = ref<ProductLine[]>([{ productCode: '', storageSlotId: '' }])
@@ -849,7 +739,7 @@
const formRules = {
productCode: [{ required: true, message: '请输入产品编码', trigger: 'blur' }],
productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
batchNo: [{ required: true, message: '请输入批次号', trigger: 'blur' }],
batchNo: [{ required: true, message: '请输入批次号', trigger: 'blur' }]
}
function createQuantityValue() {
@@ -889,7 +779,7 @@
count > 1
? buildLineProductCode(formData.productCode, i, count)
: prev[i]?.productCode || formData.productCode || '',
storageSlotId: prev[i]?.storageSlotId || '',
storageSlotId: prev[i]?.storageSlotId || ''
})
}
productLines.value = lines.length ? lines : [{ productCode: '', storageSlotId: '' }]
@@ -961,11 +851,11 @@
{ label: '储物格位置', value: row.storageSlotName || '—' },
{
label: '预计细胞数量(亿)',
value: row.expectedCellCount != null ? String(row.expectedCellCount) : '—',
value: row.expectedCellCount != null ? String(row.expectedCellCount) : '—'
},
{
label: '活性(%)',
value: row.viability != null ? String(row.viability) : '—',
value: row.viability != null ? String(row.viability) : '—'
},
{ label: '数量', value: row.quantity != null ? String(row.quantity) : '—' },
{ label: '单位', value: row.unit || '—' },
@@ -980,23 +870,23 @@
| 'danger'
| 'info'
| ''
| 'primary',
| 'primary'
},
{ label: '备注', value: row.remark || '—', span: true },
],
{ label: '备注', value: row.remark || '—', span: true }
]
},
{
title: '产品图片',
titleStyle: 'field',
slot: 'images',
fields: [],
fields: []
},
{
title: '来源链路',
titleStyle: 'field' as const,
slot: 'sourceChain',
fields: [],
},
fields: []
}
]
})
@@ -1010,7 +900,7 @@
keyword: searchForm.keyword || undefined,
productStatus: searchForm.productStatus || undefined,
passage: searchForm.passage || undefined,
sourceType: searchForm.sourceType || undefined,
sourceType: searchForm.sourceType || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
@@ -1020,10 +910,7 @@
}
async function loadStorageSlotOptions(facilities: any[]) {
storageSlotOptions.value = await loadStorageSlotSelectOptions(
facilities,
fetchStorageSlotList
)
storageSlotOptions.value = await loadStorageSlotSelectOptions(facilities, fetchStorageSlotList)
}
async function loadDropdownData() {
@@ -1031,7 +918,7 @@
const [typeRes, specRes, facilityRes] = await Promise.all([
fetchCellProductTypeList({ page: 1, pageSize: 100 }),
fetchCellProductSpecList({ page: 1, pageSize: 100 }),
fetchStorageFacilityList(),
fetchStorageFacilityList()
])
productTypeOptions.value = typeRes?.list || typeRes || []
specOptions.value = specRes?.list || specRes || []
@@ -1070,7 +957,7 @@
.filter(Boolean)
.map((url, index) => ({
fileUrl: url,
fileName: fileNameFromUrl(url, index),
fileName: fileNameFromUrl(url, index)
}))
})
@@ -1093,7 +980,7 @@
id: `existing_${i}`,
fileName: fileNameFromUrl(url, i),
fileUrl: url,
fileType: 'image/',
fileType: 'image/'
} as ProductImageFile
})
.filter((f): f is ProductImageFile => Boolean(f))
@@ -1105,15 +992,13 @@
const file = uploadFile.raw || uploadFile
if (!file) return
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
productImageFiles.value.push({
id: tempId,
fileName: file.name,
fileUrl: '',
fileType: file.type || 'image/',
_uploading: true,
_uploading: true
})
const idx = productImageFiles.value.length - 1
@@ -1128,7 +1013,7 @@
fileName: file.name || res.fileName || fileNameFromUrl(res.fileUrl, idx),
fileUrl: res.fileUrl,
fileType: res.fileType || file.type || 'image/',
_uploading: false,
_uploading: false
}
syncImagesToForm()
} catch {
@@ -1247,7 +1132,7 @@
unit: formData.unit?.trim() || undefined,
expiryDate: formData.expiryDate || undefined,
productImages: formData.productImages?.trim() || undefined,
remark: formData.remark?.trim() || undefined,
remark: formData.remark?.trim() || undefined
}
// 仅「已冻存」可变更状态;生产中/已复苏等系统态勿提交,避免后端校验失败
const editableStatuses = ['frozen', 'sold', 'used', 'voided']
@@ -1258,14 +1143,14 @@
await fetchUpdateCellProduct(editingId.value, {
...shared,
storageSlotId: formData.storageSlotId || undefined,
quantity: formData.quantity,
quantity: formData.quantity
})
ElMessage.success('更新成功')
} else {
const qty = createQuantityValue()
const items = productLines.value.map((line) => ({
productCode: line.productCode.trim(),
storageSlotId: line.storageSlotId,
storageSlotId: line.storageSlotId
}))
const res = await fetchCreateCellProduct({ ...shared, items })
const n = (res as { count?: number })?.count ?? qty
@@ -1308,7 +1193,7 @@
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个产品吗?`, '提示', {
type: 'warning',
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
@@ -37,7 +37,7 @@
EdgeLabelRenderer,
getBezierPath,
getSmoothStepPath,
type EdgeProps,
type EdgeProps
} from '@vue-flow/core'
import type { ChainEdgeData } from './chain-types'
import { CHAIN_OPEN_TASK_KEY } from './chainInject'
@@ -54,7 +54,7 @@
sourcePosition: props.sourcePosition,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition,
targetPosition: props.targetPosition
}))
const bezierCurvature = computed(() => {
@@ -70,12 +70,12 @@
if (props.data?.curved) {
return getBezierPath({
...pathInput.value,
curvature: bezierCurvature.value,
curvature: bezierCurvature.value
})
}
return getSmoothStepPath({
...pathInput.value,
borderRadius: 8,
borderRadius: 8
})
})
@@ -88,7 +88,7 @@
const hasLabel = computed(() => Boolean(workOrderLabel.value || taskLabel.value))
const labelStyle = computed(() => ({
transform: `translate(-50%, -50%) translate(${labelX.value}px, ${labelY.value}px)`,
transform: `translate(-50%, -50%) translate(${labelX.value}px, ${labelY.value}px)`
}))
function handleDblClick() {
@@ -6,7 +6,7 @@
:class="{
'is-sample': isSample,
'is-product': isProduct && !isCurrent,
'is-current': isCurrent,
'is-current': isCurrent
}"
>
<Handle type="target" :position="Position.Left" class="chain-handle" />
@@ -59,9 +59,7 @@
const isSample = computed(() => node.value.nodeType === 'sample')
const isProduct = computed(() => node.value.nodeType === 'cell_product')
const hintTitle = computed(() =>
isSample.value ? '双击查看客户信息' : '双击查看产品详情'
)
const hintTitle = computed(() => (isSample.value ? '双击查看客户信息' : '双击查看产品详情'))
const statusTag = computed(() => {
const map: Record<string, { label: string; type: 'success' | 'warning' | 'info' | 'danger' }> =
@@ -71,7 +69,7 @@
sold: { label: '已售卖', type: 'warning' },
used: { label: '已使用', type: 'info' },
voided: { label: '已作废', type: 'danger' },
thawed: { label: '已复苏', type: 'info' },
thawed: { label: '已复苏', type: 'info' }
}
const s = node.value.productStatus || ''
return map[s] || { label: s, type: 'info' as const }
@@ -32,7 +32,7 @@
const innerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val),
set: (val) => emit('update:visible', val)
})
const customer = computed(() => props.sample?.customer || {})
@@ -53,9 +53,9 @@
{ label: '母亲证件号', value: c.motherIdCard || '—' },
{ label: '新生儿姓名', value: c.newbornName || '—' },
{ label: '样本编码', value: props.sample?.sampleCode || '—' },
{ label: '协议号', value: props.sample?.protocolNo || '—' },
],
},
{ label: '协议号', value: props.sample?.protocolNo || '—' }
]
}
]
})
</script>
@@ -8,11 +8,11 @@ export const chainEdgeMarkerEnd = {
type: MarkerType.ArrowClosed,
color: CHAIN_EDGE_COLOR,
width: 12,
height: 12,
height: 12
}
export const chainDefaultEdgeOptions = {
type: 'chainLink',
markerEnd: chainEdgeMarkerEnd,
interactionWidth: 28,
interactionWidth: 28
}
@@ -1,9 +1,8 @@
import type { InjectionKey } from 'vue'
import type { ChainEdgeData, ChainNode } from './chain-types'
export const CHAIN_OPEN_TASK_KEY: InjectionKey<
(taskId: string, taskName?: string) => void
> = Symbol('chainOpenTask')
export const CHAIN_OPEN_TASK_KEY: InjectionKey<(taskId: string, taskName?: string) => void> =
Symbol('chainOpenTask')
export const CHAIN_OPEN_PRODUCT_KEY: InjectionKey<(productId: string) => void> =
Symbol('chainOpenProduct')
@@ -7,15 +7,13 @@ import type {
ChainEdgeMeta,
ChainNode,
ChainNodeData,
DescendantItem,
DescendantItem
} from './chain-types'
export const CHAIN_NODE_WIDTH = 148
export const CHAIN_NODE_HEIGHT = 100
export function nextAncestorLink(
parent: AncestorItem['parent']
): AncestorItem | null {
export function nextAncestorLink(parent: AncestorItem['parent']): AncestorItem | null {
if (!parent) return null
if (Array.isArray(parent)) return parent[0] ?? null
return parent
@@ -26,7 +24,7 @@ function makeFlowNode(n: ChainNode, isCurrent: boolean): Node<ChainNodeData> {
id: n.id,
type: 'chainNode',
position: { x: 0, y: 0 },
data: { node: n, isCurrent },
data: { node: n, isCurrent }
}
}
@@ -45,8 +43,8 @@ function makeFlowEdge(
workOrderNo: edge.workOrderNo || undefined,
taskNo: edge.taskNo || undefined,
taskId: edge.taskId || undefined,
taskName: edge.taskName || undefined,
},
taskName: edge.taskName || undefined
}
}
}
@@ -101,7 +99,7 @@ export function chainApiToElements(data: ChainApiData): {
return {
nodes: Array.from(nodeMap.values()),
edges: annotateForkEdges(edges),
edges: annotateForkEdges(edges)
}
}
@@ -119,8 +117,8 @@ function annotateForkEdges(edges: Edge<ChainEdgeData>[]): Edge<ChainEdgeData>[]
...e,
data: {
...e.data,
curved: true,
},
curved: true
}
}
})
}
@@ -139,7 +137,7 @@ export function layoutChainElements(
nodesep: 44,
ranksep: 180,
marginx: 24,
marginy: 24,
marginy: 24
})
nodes.forEach((node) => {
@@ -157,10 +155,10 @@ export function layoutChainElements(
...node,
position: {
x: pos.x - CHAIN_NODE_WIDTH / 2,
y: pos.y - CHAIN_NODE_HEIGHT / 2,
y: pos.y - CHAIN_NODE_HEIGHT / 2
},
sourcePosition: Position.Right,
targetPosition: Position.Left,
targetPosition: Position.Left
}
})
}
@@ -26,8 +26,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编号、名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -126,7 +130,13 @@
align="center"
header-align="center"
/>
<el-table-column label="操作" min-width="320" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
@@ -173,12 +183,48 @@
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column prop="consumableCode" label="耗材编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="consumableName" label="耗材名称" min-width="120" align="center" header-align="center" />
<el-table-column prop="specModel" label="规格型号" min-width="120" align="center" header-align="center" />
<el-table-column prop="standardUsage" label="标准用量" min-width="100" align="center" header-align="center" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column prop="remark" label="备注" min-width="140" align="center" header-align="center" />
<el-table-column
prop="consumableCode"
label="耗材编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="consumableName"
label="耗材名称"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="specModel"
label="规格型号"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="standardUsage"
label="标准用量"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
header-align="center"
/>
</el-table>
<el-empty v-else description="暂无耗材明细" :image-size="48" />
</template>
@@ -235,7 +281,12 @@
<div class="form-section">
<div class="section-title">
<span>耗材明细</span>
<el-button class="btn-main" type="primary" size="small" @click="openAddConsumableDialog">
<el-button
class="btn-main"
type="primary"
size="small"
@click="openAddConsumableDialog"
>
<el-icon><Plus /></el-icon>
添加耗材
</el-button>
@@ -249,10 +300,33 @@
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column prop="consumableCode" label="耗材编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="consumableName" label="耗材名称" min-width="120" align="center" header-align="center" />
<el-table-column prop="specModel" label="规格型号" min-width="120" align="center" header-align="center" />
<el-table-column label="标准用量" min-width="120" align="center" header-align="center">
<el-table-column
prop="consumableCode"
label="耗材编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="consumableName"
label="耗材名称"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="specModel"
label="规格型号"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
label="标准用量"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.standardUsage"
@@ -261,7 +335,13 @@
/>
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column label="备注" min-width="140" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
@@ -269,13 +349,22 @@
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="formData.items.splice($index, 1)">
<el-button
link
type="danger"
size="small"
@click="formData.items.splice($index, 1)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="formData.items.length === 0" description="暂无耗材,点击上方按钮添加" :image-size="48" />
<el-empty
v-if="formData.items.length === 0"
description="暂无耗材,点击上方按钮添加"
:image-size="48"
/>
</div>
</el-form>
@@ -321,12 +410,48 @@
row-key="id"
@selection-change="handleConsumableSelectionChange"
>
<el-table-column type="selection" min-width="48" align="center" header-align="center" :reserve-selection="true" />
<el-table-column prop="consumableCode" label="耗材编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="consumableName" label="耗材名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="specModel" label="规格型号" min-width="120" align="center" header-align="center" />
<el-table-column prop="standardUsage" label="标准用量" min-width="100" align="center" header-align="center" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column
type="selection"
min-width="48"
align="center"
header-align="center"
:reserve-selection="true"
/>
<el-table-column
prop="consumableCode"
label="耗材编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="consumableName"
label="耗材名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="specModel"
label="规格型号"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="standardUsage"
label="标准用量"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
prop="description"
label="描述"
@@ -581,7 +706,9 @@
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
try {
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个耗材包吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个耗材包吗?`, '提示', {
type: 'warning'
})
} catch {
return
}
@@ -4,7 +4,9 @@
<div class="page-container">
<header class="page-head">
<h1>耗材管理</h1>
<p class="page-desc">维护生产耗材的编码、名称、规格、标准用量及单位等信息,支持入库、出库操作。</p>
<p class="page-desc"
>维护生产耗材的编码、名称、规格、标准用量及单位等信息,支持入库、出库操作。</p
>
</header>
<section class="panel">
@@ -30,8 +32,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编码、名称模糊查询,可按状态筛选。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -76,7 +82,12 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="selection"
min-width="55"
align="center"
header-align="center"
/>
<el-table-column
type="index"
label="序号"
@@ -85,18 +96,64 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="consumableCode" label="耗材编码" min-width="130" align="center" header-align="center" />
<el-table-column prop="consumableName" label="耗材名称" min-width="150" align="center" header-align="center" />
<el-table-column prop="specModel" label="规格型号" min-width="120" align="center" header-align="center" />
<el-table-column prop="standardUsage" label="标准用量" min-width="100" align="center" header-align="center" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column prop="stockQty" label="当前库存" min-width="100" align="center" header-align="center" />
<el-table-column label="预警库存" min-width="100" align="center" header-align="center">
<el-table-column
prop="consumableCode"
label="耗材编码"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="consumableName"
label="耗材名称"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="specModel"
label="规格型号"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="standardUsage"
label="标准用量"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
prop="stockQty"
label="当前库存"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
label="预警库存"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ row.warningQty ?? 0 }}
</template>
</el-table-column>
<el-table-column label="库存状态" min-width="100" align="center" header-align="center">
<el-table-column
label="库存状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="isStockLow(row) ? 'danger' : 'success'" size="small">
{{ isStockLow(row) ? '库存预警' : '正常' }}
@@ -110,14 +167,32 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="480" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="480"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleStockIn(row)"><el-icon><Upload /></el-icon>入库</el-button>
<el-button class="btn-op-item" @click="handleStockOut(row)"><el-icon><Download /></el-icon>出库</el-button>
<el-button class="btn-op-item" @click="handleViewStockLog(row)"><el-icon><Document /></el-icon>日志</el-button>
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item primary" @click="handleEdit(row)"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)"><el-icon><Delete /></el-icon>删除</el-button>
<el-button class="btn-op-item" @click="handleStockIn(row)"
><el-icon><Upload /></el-icon>入库</el-button
>
<el-button class="btn-op-item" @click="handleStockOut(row)"
><el-icon><Download /></el-icon>出库</el-button
>
<el-button class="btn-op-item" @click="handleViewStockLog(row)"
><el-icon><Document /></el-icon>日志</el-button
>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="handleEdit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDelete(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -128,7 +203,9 @@
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@size-change="
(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())
"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
@@ -165,8 +242,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按耗材编码/名称、变动类型和时间范围查询变动记录。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleLogReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleLogSearch"><el-icon><Search /></el-icon>搜索</el-button>
<el-button class="btn-ghost" @click="handleLogReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
>
<el-button class="btn-main" type="primary" @click="handleLogSearch"
><el-icon><Search /></el-icon>搜索</el-button
>
</div>
</div>
</el-form>
@@ -189,31 +270,75 @@
header-align="center"
:index="(index) => (logPagination.page - 1) * logPagination.pageSize + index + 1"
/>
<el-table-column prop="materialCode" label="耗材编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="materialName" label="耗材名称" min-width="140" align="center" header-align="center" />
<el-table-column label="变动类型" min-width="100" align="center" header-align="center">
<el-table-column
prop="materialCode"
label="耗材编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="耗材名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
label="变动类型"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="getChangeTypeTagType(row)" size="small">
{{ getChangeTypeLabel(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="变动数量" min-width="120" align="center" header-align="center">
<el-table-column
label="变动数量"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<span :class="row.changeQty < 0 ? 'qty-negative' : 'qty-positive'">
{{ row.changeQty < 0 ? row.changeQty : `+${row.changeQty}` }}
</span>
</template>
</el-table-column>
<el-table-column prop="stockBefore" label="变动前库存" min-width="120" align="center" header-align="center" />
<el-table-column prop="stockAfter" label="变动后库存" min-width="120" align="center" header-align="center" />
<el-table-column
prop="stockBefore"
label="变动前库存"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="stockAfter"
label="变动后库存"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column label="工单号" min-width="140" align="center" header-align="center">
<template #default="{ row }">{{ row.taskNo || '-' }}</template>
</el-table-column>
<el-table-column label="步骤名称" min-width="140" align="center" header-align="center">
<el-table-column
label="步骤名称"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stepName || '-' }}</template>
</el-table-column>
<el-table-column label="来源类型" min-width="120" align="center" header-align="center">
<el-table-column
label="来源类型"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="getSourceTagType(row.sourceType)" size="small">
{{ getSourceLabel(row.sourceType) }}
@@ -223,18 +348,50 @@
<el-table-column label="领用人" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ displayLogReceiver(row) }}</template>
</el-table-column>
<el-table-column label="用途" min-width="120" align="center" header-align="center" show-overflow-tooltip>
<el-table-column
label="用途"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ displayLogPurpose(row) }}</template>
</el-table-column>
<el-table-column label="领用时间" min-width="170" align="center" header-align="center">
<el-table-column
label="领用时间"
min-width="170"
align="center"
header-align="center"
>
<template #default="{ row }">{{ displayLogUseTime(row) }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" align="center" header-align="center" show-overflow-tooltip />
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="操作人" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ row.createByName || row.createBy || '-' }}</template>
<template #default="{ row }">{{
row.createByName || row.createBy || '-'
}}</template>
</el-table-column>
<el-table-column prop="createTime" label="变动时间" min-width="170" align="center" header-align="center" />
<el-table-column label="操作" min-width="160" fixed="right" align="center" header-align="center">
<el-table-column
prop="createTime"
label="变动时间"
min-width="170"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="160"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleLogView(row)">
<el-icon><View /></el-icon>查看
@@ -249,7 +406,9 @@
:total="logPagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((logPagination.page = 1), (logPagination.pageSize = val), loadLogData())"
@size-change="
(val) => ((logPagination.page = 1), (logPagination.pageSize = val), loadLogData())
"
@current-change="(val) => ((logPagination.page = val), loadLogData())"
/>
</div>
@@ -300,7 +459,9 @@
</el-select>
</el-form-item>
<el-form-item label="预警库存">
<template #label>预警库存<span class="form-label-tip">低于该值时列表显示库存预警</span></template>
<template #label
>预警库存<span class="form-label-tip">低于该值时列表显示库存预警</span></template
>
<el-input-number
v-model="formData.warningQty"
:min="0"
@@ -317,7 +478,12 @@
</el-radio-group>
</el-form-item>
<el-form-item label="描述" class="form-col-span-2">
<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入描述" />
<el-input
v-model="formData.description"
type="textarea"
:rows="3"
placeholder="请输入描述"
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="formData.remark" placeholder="可选" />
@@ -365,12 +531,7 @@
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="stockInForm.remark"
type="textarea"
:rows="3"
placeholder="可选"
/>
<el-input v-model="stockInForm.remark" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
@@ -440,20 +601,10 @@
</el-select>
</el-form-item>
<el-form-item label="用途">
<el-input
v-model="stockOutForm.purpose"
type="textarea"
:rows="2"
placeholder="可选"
/>
<el-input v-model="stockOutForm.purpose" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="stockOutForm.remark"
type="textarea"
:rows="2"
placeholder="可选"
/>
<el-input v-model="stockOutForm.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
@@ -544,13 +695,41 @@
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column prop="materialCode" label="耗材编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="materialName" label="耗材名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column label="当前库存" min-width="100" align="center" header-align="center">
<el-table-column
prop="materialCode"
label="耗材编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="耗材名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
label="当前库存"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stockQty }}</template>
</el-table-column>
<el-table-column label="入库数量" min-width="130" align="center" header-align="center">
<el-table-column
label="入库数量"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input-number
v-model="row.qty"
@@ -564,22 +743,28 @@
</el-table-column>
<el-table-column label="备注" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="可选" clearable class="inner-cell-input" />
<el-input
v-model="row.remark"
placeholder="可选"
clearable
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="batchStockInLines.splice($index, 1)">
<el-button
link
type="danger"
size="small"
@click="batchStockInLines.splice($index, 1)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="请通过上方搜索框添加要入库的耗材"
:image-size="56"
/>
<el-empty v-else description="请通过上方搜索框添加要入库的耗材" :image-size="56" />
</div>
</el-form>
<template #footer>
@@ -699,13 +884,41 @@
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column prop="materialCode" label="耗材编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="materialName" label="耗材名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column label="当前库存" min-width="100" align="center" header-align="center">
<el-table-column
prop="materialCode"
label="耗材编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="耗材名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
label="当前库存"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stockQty }}</template>
</el-table-column>
<el-table-column label="出库数量" min-width="130" align="center" header-align="center">
<el-table-column
label="出库数量"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input-number
v-model="row.qty"
@@ -720,22 +933,28 @@
</el-table-column>
<el-table-column label="备注" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="可选" clearable class="inner-cell-input" />
<el-input
v-model="row.remark"
placeholder="可选"
clearable
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="batchStockOutLines.splice($index, 1)">
<el-button
link
type="danger"
size="small"
@click="batchStockOutLines.splice($index, 1)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="请通过上方搜索框添加要出库的耗材"
:image-size="56"
/>
<el-empty v-else description="请通过上方搜索框添加要出库的耗材" :image-size="56" />
</div>
</el-form>
<template #footer>
@@ -778,7 +997,9 @@
min-width="56"
align="center"
header-align="center"
:index="(index) => (stockLogPagination.page - 1) * stockLogPagination.pageSize + index + 1"
:index="
(index) => (stockLogPagination.page - 1) * stockLogPagination.pageSize + index + 1
"
/>
<el-table-column label="变动类型" min-width="100" align="center" header-align="center">
<template #default="{ row }">
@@ -794,8 +1015,20 @@
</span>
</template>
</el-table-column>
<el-table-column prop="stockBefore" label="变动前库存" min-width="120" align="center" header-align="center" />
<el-table-column prop="stockAfter" label="变动后库存" min-width="120" align="center" header-align="center" />
<el-table-column
prop="stockBefore"
label="变动前库存"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="stockAfter"
label="变动后库存"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column label="来源类型" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="getSourceTagType(row.sourceType)" size="small">
@@ -812,14 +1045,33 @@
<el-table-column label="领用人" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ row.receiver || '-' }}</template>
</el-table-column>
<el-table-column label="用途" min-width="120" align="center" header-align="center" show-overflow-tooltip>
<el-table-column
label="用途"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.purpose || '-' }}</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" align="center" header-align="center" show-overflow-tooltip />
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="操作人" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ row.createByName || row.createBy || '-' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="变动时间" min-width="170" align="center" header-align="center" />
<el-table-column
prop="createTime"
label="变动时间"
min-width="170"
align="center"
header-align="center"
/>
</el-table>
</div>
<div class="pagination list-pager-bar" style="padding-top: 12px">
@@ -829,7 +1081,13 @@
:total="stockLogPagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((stockLogPagination.page = 1), (stockLogPagination.pageSize = val), loadStockLogData())"
@size-change="
(val) => (
(stockLogPagination.page = 1),
(stockLogPagination.pageSize = val),
loadStockLogData()
)
"
@current-change="(val) => ((stockLogPagination.page = val), loadStockLogData())"
/>
</div>
@@ -1304,7 +1562,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个耗材吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个耗材吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteConsumable(id)
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编码、名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -68,10 +72,34 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="typeCode" label="类型编码" min-width="150" align="center" header-align="center" />
<el-table-column prop="typeName" 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 label="操作" min-width="320" fixed="right" align="center" header-align="center">
<el-table-column
prop="typeCode"
label="类型编码"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="typeName"
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
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -151,7 +179,15 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, ScaleToOriginal, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
ScaleToOriginal,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
@@ -304,7 +340,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个类型吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个类型吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteOutputType(id)
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编码、名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -68,10 +72,34 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="categoryCode" label="分类编码" min-width="150" align="center" header-align="center" />
<el-table-column prop="categoryName" 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 label="操作" min-width="320" fixed="right" align="center" header-align="center">
<el-table-column
prop="categoryCode"
label="分类编码"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column
prop="categoryName"
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
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -151,7 +179,15 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, ScaleToOriginal, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
ScaleToOriginal,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
@@ -304,7 +340,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个分类吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个分类吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteProcessCategory(id)
@@ -31,12 +31,7 @@
/>
</el-form-item>
<el-form-item label="产品类型">
<el-select
v-model="searchForm.productTypeId"
placeholder="全部"
clearable
filterable
>
<el-select v-model="searchForm.productTypeId" placeholder="全部" clearable filterable>
<el-option
v-for="pt in productTypeOptions"
:key="pt.id"
@@ -49,8 +44,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按路线名称、编码、产品类型筛选。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -94,33 +93,68 @@
header-align="center"
:index="(index: number) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="routeName" label="路线名称" min-width="160" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="routeCode" label="路线编码" min-width="130" align="center" header-align="center" show-overflow-tooltip />
<el-table-column
prop="routeName"
label="路线名称"
min-width="160"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="routeCode"
label="路线编码"
min-width="130"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="产品类型" min-width="130" align="center" header-align="center">
<template #default="{ row }">
{{ productTypeMap[row.productTypeId] || '-' }}
</template>
</el-table-column>
<el-table-column prop="version" label="版本号" min-width="90" align="center" header-align="center" />
<el-table-column
prop="version"
label="版本号"
min-width="90"
align="center"
header-align="center"
/>
<el-table-column label="当前版本" min-width="90" align="center" header-align="center">
<template #default="{ row }">
<el-tag v-if="row.isCurrent" type="success" size="small">当前</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" header-align="center" />
<el-table-column label="操作" min-width="340" fixed="right" align="center" header-align="center">
<el-table-column
prop="remark"
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="createTime"
label="创建时间"
min-width="170"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="340"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleOpenConfig(row)">
<el-icon><Rank /></el-icon>路线图配置
</el-button>
<el-button class="btn-op-item" @click="handleEdit(row)">编辑</el-button>
<el-button
v-if="!row.isCurrent"
class="btn-op-item"
@click="handleSetCurrent(row)"
>
<el-button v-if="!row.isCurrent" class="btn-op-item" @click="handleSetCurrent(row)">
设为当前
</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
@@ -134,7 +168,9 @@
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val: number) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@size-change="
(val: number) => ((pagination.page = 1), (pagination.pageSize = val), loadData())
"
@current-change="(val: number) => ((pagination.page = val), loadData())"
/>
</div>
@@ -167,7 +203,12 @@
<el-input v-model="formData.routeName" placeholder="请输入路线名称" />
</el-form-item>
<el-form-item label="产品类型">
<el-select v-model="formData.productTypeId" placeholder="请选择产品类型" clearable filterable>
<el-select
v-model="formData.productTypeId"
placeholder="请选择产品类型"
clearable
filterable
>
<el-option
v-for="pt in productTypeOptions"
:key="pt.id"
@@ -238,7 +279,8 @@
>
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span>{{ proc.processName }}
<span class="route-config-process-seq">{{ procIndex + 1 }}</span
>{{ proc.processName }}
</div>
<div class="route-config-process-code">{{ proc.processCode }}</div>
</div>
@@ -288,7 +330,9 @@
</div>
<div class="route-config-step-order">{{ index + 1 }}</div>
<div class="route-config-step-info">
<div class="route-config-step-name">{{ element.processName || '未命名步骤' }}</div>
<div class="route-config-step-name">{{
element.processName || '未命名步骤'
}}</div>
<div class="route-config-step-desc">{{ element.description || '' }}</div>
</div>
<el-button
@@ -357,7 +401,11 @@
<el-input v-model="selectedStep.equipmentName" @change="saveStepBasic" />
</el-form-item>
<el-form-item label="预计时长(min)">
<el-input v-model="selectedStep.estimatedDuration" placeholder="如 2h" @change="saveStepBasic" />
<el-input
v-model="selectedStep.estimatedDuration"
placeholder="如 2h"
@change="saveStepBasic"
/>
</el-form-item>
<el-form-item label="描述" class="step-form-col-half">
<el-input
@@ -384,10 +432,7 @@
/>
</el-select>
</el-form-item>
<div
v-if="sopIdsArr.length"
class="step-sop-attachment-row step-form-col-full"
>
<div v-if="sopIdsArr.length" class="step-sop-attachment-row step-form-col-full">
<ProtocolAttachmentReadonly
:files="linkedSopAttachmentFiles"
@preview="handleSopAttachmentPreview"
@@ -401,12 +446,22 @@
<!-- 试剂 -->
<el-tab-pane label="试剂" name="reagent">
<div class="route-config-tab-toolbar">
<el-button class="btn-main" type="primary" size="small" @click="addReagentRow">
<el-button
class="btn-main"
type="primary"
size="small"
@click="addReagentRow"
>
<el-icon><Plus /></el-icon>
添加试剂
</el-button>
</div>
<el-table :data="selectedStep.reagents" stripe class="inner-table" max-height="300">
<el-table
:data="selectedStep.reagents"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
@@ -415,12 +470,27 @@
header-align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="试剂编码" min-width="110" align="center" header-align="center">
<el-table-column
label="试剂编码"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.reagentCode" placeholder="编码" class="inner-cell-input" @change="saveReagents" />
<el-input
v-model="row.reagentCode"
placeholder="编码"
class="inner-cell-input"
@change="saveReagents"
/>
</template>
</el-table-column>
<el-table-column label="试剂名称" min-width="160" align="center" header-align="center">
<el-table-column
label="试剂名称"
min-width="160"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.reagentId"
@@ -444,17 +514,42 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="规格" min-width="100" align="center" header-align="center">
<el-table-column
label="规格"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.spec" placeholder="规格" class="inner-cell-input" @change="saveReagents" />
<el-input
v-model="row.spec"
placeholder="规格"
class="inner-cell-input"
@change="saveReagents"
/>
</template>
</el-table-column>
<el-table-column label="标准用量" min-width="110" align="center" header-align="center">
<el-table-column
label="标准用量"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.standardUsage" placeholder="用量" class="inner-cell-input" @change="saveReagents" />
<el-input
v-model="row.standardUsage"
placeholder="用量"
class="inner-cell-input"
@change="saveReagents"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="120" align="center" header-align="center">
<el-table-column
label="单位"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.unit"
@@ -478,34 +573,74 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="关键物料" min-width="90" align="center" header-align="center">
<el-table-column
label="关键物料"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-switch v-model="row.isKeyMaterial" @change="saveReagents" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center" header-align="center">
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" @change="saveReagents" />
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="saveReagents"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<el-table-column
label="操作"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="removeReagentRow($index)">删除</el-button>
<el-button
link
type="danger"
size="small"
@click="removeReagentRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!selectedStep.reagents || selectedStep.reagents.length === 0" description="暂无试剂" :image-size="48" />
<el-empty
v-if="!selectedStep.reagents || selectedStep.reagents.length === 0"
description="暂无试剂"
:image-size="48"
/>
</el-tab-pane>
<!-- 耗材 -->
<el-tab-pane label="耗材" name="consumable">
<div class="route-config-tab-toolbar">
<el-button class="btn-main" type="primary" size="small" @click="addConsumableRow">
<el-button
class="btn-main"
type="primary"
size="small"
@click="addConsumableRow"
>
<el-icon><Plus /></el-icon>
添加耗材
</el-button>
</div>
<el-table :data="selectedStep.consumables" stripe class="inner-table" max-height="300">
<el-table
:data="selectedStep.consumables"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
@@ -514,12 +649,27 @@
header-align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="耗材编码" min-width="110" align="center" header-align="center">
<el-table-column
label="耗材编码"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.consumableCode" placeholder="编码" class="inner-cell-input" @change="saveConsumables" />
<el-input
v-model="row.consumableCode"
placeholder="编码"
class="inner-cell-input"
@change="saveConsumables"
/>
</template>
</el-table-column>
<el-table-column label="耗材名称" min-width="160" align="center" header-align="center">
<el-table-column
label="耗材名称"
min-width="160"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.consumableId"
@@ -543,17 +693,42 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="规格型号" min-width="110" align="center" header-align="center">
<el-table-column
label="规格型号"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.specModel" placeholder="规格型号" class="inner-cell-input" @change="saveConsumables" />
<el-input
v-model="row.specModel"
placeholder="规格型号"
class="inner-cell-input"
@change="saveConsumables"
/>
</template>
</el-table-column>
<el-table-column label="标准用量" min-width="110" align="center" header-align="center">
<el-table-column
label="标准用量"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.standardUsage" placeholder="用量" class="inner-cell-input" @change="saveConsumables" />
<el-input
v-model="row.standardUsage"
placeholder="用量"
class="inner-cell-input"
@change="saveConsumables"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="120" align="center" header-align="center">
<el-table-column
label="单位"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.unit"
@@ -577,23 +752,53 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="必需" min-width="80" align="center" header-align="center">
<el-table-column
label="必需"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-switch v-model="row.isRequired" @change="saveConsumables" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center" header-align="center">
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" @change="saveConsumables" />
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="saveConsumables"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<el-table-column
label="操作"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="removeConsumableRow($index)">删除</el-button>
<el-button
link
type="danger"
size="small"
@click="removeConsumableRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!selectedStep.consumables || selectedStep.consumables.length === 0" description="暂无耗材" :image-size="48" />
<el-empty
v-if="!selectedStep.consumables || selectedStep.consumables.length === 0"
description="暂无耗材"
:image-size="48"
/>
</el-tab-pane>
<!-- 产出物 -->
@@ -604,7 +809,12 @@
添加产出物
</el-button>
</div>
<el-table :data="selectedStep.outputs" stripe class="inner-table" max-height="300">
<el-table
:data="selectedStep.outputs"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
@@ -613,9 +823,20 @@
header-align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="产出类型" min-width="150" align="center" header-align="center">
<el-table-column
label="产出类型"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select v-model="row.outputTypeId" placeholder="选择类型" filterable class="inner-cell-select" @change="saveOutputs">
<el-select
v-model="row.outputTypeId"
placeholder="选择类型"
filterable
class="inner-cell-select"
@change="saveOutputs"
>
<el-option
v-for="ot in outputTypeOptions"
:key="ot.id"
@@ -625,39 +846,94 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="名称" min-width="150" align="center" header-align="center">
<el-table-column
label="名称"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.outputName" placeholder="名称" class="inner-cell-input" @change="saveOutputs" />
<el-input
v-model="row.outputName"
placeholder="名称"
class="inner-cell-input"
@change="saveOutputs"
/>
</template>
</el-table-column>
<el-table-column label="条码" min-width="150" align="center" header-align="center">
<el-table-column
label="条码"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.outputBarcode" placeholder="条码" class="inner-cell-input" @change="saveOutputs" />
<el-input
v-model="row.outputBarcode"
placeholder="条码"
class="inner-cell-input"
@change="saveOutputs"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center" header-align="center">
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" @change="saveOutputs" />
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="saveOutputs"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<el-table-column
label="操作"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="removeOutputRow($index)">删除</el-button>
<el-button
link
type="danger"
size="small"
@click="removeOutputRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!selectedStep.outputs || selectedStep.outputs.length === 0" description="暂无产出物" :image-size="48" />
<el-empty
v-if="!selectedStep.outputs || selectedStep.outputs.length === 0"
description="暂无产出物"
:image-size="48"
/>
</el-tab-pane>
<!-- 数据采集 -->
<el-tab-pane label="数据采集" name="dataCollection">
<div class="route-config-tab-toolbar">
<el-button class="btn-main" type="primary" size="small" @click="addDataCollectionRow">
<el-button
class="btn-main"
type="primary"
size="small"
@click="addDataCollectionRow"
>
<el-icon><Plus /></el-icon>
添加采集项
</el-button>
</div>
<el-table :data="selectedStep.dataCollections" stripe class="inner-table" max-height="300">
<el-table
:data="selectedStep.dataCollections"
stripe
class="inner-table"
max-height="300"
>
<el-table-column
type="index"
label="序号"
@@ -666,12 +942,27 @@
header-align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="采集名称" min-width="180" align="center" header-align="center">
<el-table-column
label="采集名称"
min-width="180"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.collectionName" placeholder="采集项名称" class="inner-cell-input" @change="saveDataCollections" />
<el-input
v-model="row.collectionName"
placeholder="采集项名称"
class="inner-cell-input"
@change="saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="120" align="center" header-align="center">
<el-table-column
label="单位"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.unit"
@@ -695,18 +986,45 @@
</el-select>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center" header-align="center">
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" @change="saveDataCollections" />
<el-input
v-model="row.remark"
placeholder="备注"
class="inner-cell-input"
@change="saveDataCollections"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<el-table-column
label="操作"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="removeDataCollectionRow($index)">删除</el-button>
<el-button
link
type="danger"
size="small"
@click="removeDataCollectionRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!selectedStep.dataCollections || selectedStep.dataCollections.length === 0" description="暂无采集项" :image-size="48" />
<el-empty
v-if="
!selectedStep.dataCollections || selectedStep.dataCollections.length === 0
"
description="暂无采集项"
:image-size="48"
/>
</el-tab-pane>
</el-tabs>
</div>
@@ -726,7 +1044,16 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Plus, Delete, Rank, Close, Check, CopyDocument, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Delete,
Rank,
Close,
Check,
CopyDocument,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { VueDraggable as draggable } from 'vue-draggable-plus'
@@ -918,7 +1245,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条路线吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条路线吗?`, '提示', {
type: 'warning'
})
for (const id of ids) {
await fetchDeleteProcessRoute(id)
}
@@ -971,7 +1300,28 @@
// 工艺库数据
const processLibrary = ref<Record<string, any>[]>([])
const expandedCategories = ref<string[]>([])
const chineseOrdinals = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十']
const chineseOrdinals = [
'一',
'二',
'三',
'四',
'五',
'六',
'七',
'八',
'九',
'十',
'十一',
'十二',
'十三',
'十四',
'十五',
'十六',
'十七',
'十八',
'十九',
'二十'
]
const categoryOptions = ref<Record<string, any>[]>([])
const sopOptions = ref<Record<string, any>[]>([])
const outputTypeOptions = ref<Record<string, any>[]>([])
@@ -1088,7 +1438,10 @@
const legacyId = `legacy:${code || name}`
row.reagentId = legacyId
if (!reagentOptions.value.some((o) => o.id === legacyId)) {
reagentOptions.value = [{ id: legacyId, label: mapReagentOptionLabel(row) }, ...reagentOptions.value]
reagentOptions.value = [
{ id: legacyId, label: mapReagentOptionLabel(row) },
...reagentOptions.value
]
}
}
@@ -1205,7 +1558,10 @@
const legacyId = `legacy:${code || name}`
row.consumableId = legacyId
if (!consumableOptions.value.some((o) => o.id === legacyId)) {
consumableOptions.value = [{ id: legacyId, label: mapConsumableOptionLabel(row) }, ...consumableOptions.value]
consumableOptions.value = [
{ id: legacyId, label: mapConsumableOptionLabel(row) },
...consumableOptions.value
]
}
}
@@ -1256,7 +1612,9 @@
for (const step of stepList.value) {
;(step.reagents || []).forEach((r: Record<string, any>) => ensureItemUnitOption(r.unit))
;(step.consumables || []).forEach((c: Record<string, any>) => ensureItemUnitOption(c.unit))
;(step.dataCollections || []).forEach((d: Record<string, any>) => ensureItemUnitOption(d.unit))
;(step.dataCollections || []).forEach((d: Record<string, any>) =>
ensureItemUnitOption(d.unit)
)
}
}
@@ -1323,8 +1681,7 @@
files.push({
id: f.id || `${sop.id}_${f.fileUrl}`,
fileName:
f.fileName ||
decodeURIComponent(String(f.fileUrl).split('/').pop() || '文档'),
f.fileName || decodeURIComponent(String(f.fileUrl).split('/').pop() || '文档'),
fileUrl: f.fileUrl,
fileType: f.fileType || ''
})
@@ -1379,10 +1736,16 @@
outputTypeOptions.value = outputTypes || []
// 按分类分组
const catMap: Record<string, { categoryId: string; categoryName: string; processes: Record<string, any>[] }> = {}
const catMap: Record<
string,
{ categoryId: string; categoryName: string; processes: Record<string, any>[] }
> = {}
const uncategorizedKey = '__uncategorized__'
catMap[uncategorizedKey] = { categoryId: uncategorizedKey, categoryName: '未分类', processes: [] }
catMap[uncategorizedKey] = {
categoryId: uncategorizedKey,
categoryName: '未分类',
processes: []
}
;(categories || []).forEach((cat: Record<string, any>) => {
catMap[cat.id] = {
categoryId: cat.id,
@@ -1390,7 +1753,6 @@
processes: []
}
})
;(processes || []).forEach((proc: Record<string, any>) => {
const catId = proc.categoryId || uncategorizedKey
if (!catMap[catId]) {
@@ -1402,7 +1764,11 @@
// 展开所有分类
expandedCategories.value = Object.keys(catMap)
processLibrary.value = Object.values(catMap)
await Promise.all([searchItemUnitOptions(''), searchReagentOptions(''), searchConsumableOptions('')])
await Promise.all([
searchItemUnitOptions(''),
searchReagentOptions(''),
searchConsumableOptions('')
])
} catch {
/* ignore */
}
@@ -1433,7 +1799,9 @@
async function loadRouteDetail() {
try {
const detail = await fetchProcessRouteDetail(configRouteId.value)
stepList.value = (detail.steps || []).map((s: Record<string, any>) => normalizeStepSubTables(s))
stepList.value = (detail.steps || []).map((s: Record<string, any>) =>
normalizeStepSubTables(s)
)
ensureAllStepReagentOptions()
ensureAllStepConsumableOptions()
ensureAllStepUnitOptions()
@@ -1494,7 +1862,7 @@
selectedStepId.value = step.id
}
async function handleCopyStep(step: Record<string, any>) {
async function handleCopyStep(step: Record<string, any>) {
stepListLoading.value = true
try {
const newStep = await fetchCopyRouteStep({ stepId: step.id })
@@ -1580,9 +1948,7 @@
const seen = new Map<string, number>()
for (let i = 0; i < reagents.length; i++) {
const r = reagents[i]
const key = r.reagentId
? String(r.reagentId)
: (r.reagentCode || '').trim()
const key = r.reagentId ? String(r.reagentId) : (r.reagentCode || '').trim()
if (!key) continue
if (seen.has(key)) {
ElMessage.warning(`试剂"${r.reagentName || r.reagentCode}"重复,请移除重复项`)
@@ -1638,9 +2004,7 @@
const seen = new Map<string, number>()
for (let i = 0; i < consumables.length; i++) {
const c = consumables[i]
const key = c.consumableId
? String(c.consumableId)
: (c.consumableCode || '').trim()
const key = c.consumableId ? String(c.consumableId) : (c.consumableCode || '').trim()
if (!key) continue
if (seen.has(key)) {
ElMessage.warning(`耗材"${c.consumableName || c.consumableCode}"重复,请移除重复项`)
@@ -1732,12 +2096,14 @@
try {
await fetchUpdateStepDataCollections({
stepId: selectedStep.value.id,
items: (selectedStep.value.dataCollections || []).map((d: Record<string, any>, idx: number) => ({
collectionName: d.collectionName,
unit: d.unit,
orderNum: idx,
remark: d.remark || ''
}))
items: (selectedStep.value.dataCollections || []).map(
(d: Record<string, any>, idx: number) => ({
collectionName: d.collectionName,
unit: d.unit,
orderNum: idx,
remark: d.remark || ''
})
)
})
} catch {
/* ignore */
+603 -273
View File
@@ -26,8 +26,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按工序代码、名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -85,12 +89,7 @@
align="center"
header-align="center"
/>
<el-table-column
label="工序分类"
min-width="130"
align="center"
header-align="center"
>
<el-table-column label="工序分类" min-width="130" align="center" header-align="center">
<template #default="{ row }">
{{ categoryMap[row.categoryId] || '-' }}
</template>
@@ -124,7 +123,13 @@
align="center"
header-align="center"
/>
<el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
@@ -182,63 +187,68 @@
<div class="section-title-left"><h3>基本信息</h3></div>
</div>
<div class="form-grid">
<el-form-item label="工序代码" prop="processCode">
<el-input v-model="formData.processCode" placeholder="请输入工序代码" />
</el-form-item>
<el-form-item label="工序名称" prop="processName">
<el-input v-model="formData.processName" placeholder="请输入工序名称" />
</el-form-item>
<el-form-item label="工序分类">
<el-select
v-model="formData.categoryId"
placeholder="请选择分类"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="cat in categoryOptions"
:key="cat.id"
:label="cat.categoryName || cat.name"
:value="cat.id"
<el-form-item label="工序代码" prop="processCode">
<el-input v-model="formData.processCode" placeholder="请输入工序代码" />
</el-form-item>
<el-form-item label="工序名称" prop="processName">
<el-input v-model="formData.processName" placeholder="请输入工序名称" />
</el-form-item>
<el-form-item label="工序分类">
<el-select
v-model="formData.categoryId"
placeholder="请选择分类"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="cat in categoryOptions"
:key="cat.id"
:label="cat.categoryName || cat.name"
:value="cat.id"
/>
</el-select>
</el-form-item>
<el-form-item label="设备名称">
<el-input v-model="formData.equipmentName" placeholder="请输入设备名称" />
</el-form-item>
<el-form-item>
<template #label>预估耗时<span class="form-label-tip">(min)</span></template>
<el-input v-model="formData.estimatedDuration" placeholder="如 30" />
</el-form-item>
<el-form-item label="关联SOP">
<el-select
v-model="formData.sopIds"
placeholder="请选择SOP文档"
multiple
clearable
filterable
style="width: 100%"
>
<el-option
v-for="sop in sopOptions"
:key="sop.id"
:label="sop.sopName || sop.name"
:value="sop.id"
/>
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input
v-model="formData.description"
type="textarea"
:rows="2"
placeholder="请输入描述"
/>
</el-select>
</el-form-item>
<el-form-item label="设备名称">
<el-input v-model="formData.equipmentName" placeholder="请输入设备名称" />
</el-form-item>
<el-form-item>
<template #label>预估耗时<span class="form-label-tip">(min)</span></template>
<el-input v-model="formData.estimatedDuration" placeholder="如 30" />
</el-form-item>
<el-form-item label="关联SOP">
<el-select
v-model="formData.sopIds"
placeholder="请选择SOP文档"
multiple
clearable
filterable
style="width: 100%"
>
<el-option
v-for="sop in sopOptions"
:key="sop.id"
:label="sop.sopName || sop.name"
:value="sop.id"
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="formData.remark"
type="textarea"
:rows="2"
placeholder="可选"
/>
</el-select>
</el-form-item>
<el-form-item label="描述">
<el-input
v-model="formData.description"
type="textarea"
:rows="2"
placeholder="请输入描述"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</el-form-item>
</div>
</div>
</div>
@@ -247,50 +257,104 @@
<div class="section-card">
<div class="section-title">
<div class="section-title-left"><h3>试剂关联</h3></div>
<el-button class="btn-main" type="primary" size="small" @click="openAddReagentDialog">
<el-button
class="btn-main"
type="primary"
size="small"
@click="openAddReagentDialog"
>
<el-icon><Plus /></el-icon>
添加试剂
</el-button>
</div>
<el-table :data="reagentList" stripe class="inner-table">
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
<el-table :data="reagentList" stripe class="inner-table">
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column
prop="reagentCode"
label="试剂编码"
min-width="110"
align="center"
header-align="center"
/>
<el-table-column
prop="reagentName"
label="试剂名称"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="spec"
label="规格"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
label="标准用量"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.standardUsage"
placeholder="用量"
class="inner-cell-input inner-cell-input--usage"
/>
</template>
</el-table-column>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
label="关键物料"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-switch v-model="row.isKeyMaterial" />
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button
link
type="danger"
size="small"
@click="reagentList.splice($index, 1)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="reagentList.length === 0"
description="暂无试剂,点击上方按钮添加"
:image-size="48"
/>
<el-table-column prop="reagentCode" label="试剂编码" min-width="110" align="center" header-align="center" />
<el-table-column prop="reagentName" label="试剂名称" min-width="120" align="center" header-align="center" />
<el-table-column prop="spec" label="规格" min-width="100" align="center" header-align="center" />
<el-table-column label="标准用量" min-width="110" align="center" header-align="center">
<template #default="{ row }">
<el-input
v-model="row.standardUsage"
placeholder="用量"
class="inner-cell-input inner-cell-input--usage"
/>
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column label="关键物料" min-width="90" align="center" header-align="center">
<template #default="{ row }">
<el-switch v-model="row.isKeyMaterial" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="reagentList.splice($index, 1)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="reagentList.length === 0" description="暂无试剂,点击上方按钮添加" :image-size="48" />
</div>
</div>
@@ -299,7 +363,12 @@
<div class="section-title">
<div class="section-title-left"><h3>耗材关联</h3></div>
<div class="section-title-actions">
<el-button class="btn-main" type="primary" size="small" @click="openAddConsumableDialog">
<el-button
class="btn-main"
type="primary"
size="small"
@click="openAddConsumableDialog"
>
<el-icon><Plus /></el-icon>
添加耗材
</el-button>
@@ -308,45 +377,89 @@
</el-button>
</div>
</div>
<el-table :data="consumableList" stripe class="inner-table">
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
<el-table :data="consumableList" stripe class="inner-table">
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column
prop="consumableCode"
label="耗材编码"
min-width="110"
align="center"
header-align="center"
/>
<el-table-column
prop="consumableName"
label="耗材名称"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="specModel"
label="规格型号"
min-width="110"
align="center"
header-align="center"
/>
<el-table-column
label="标准用量"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.standardUsage"
placeholder="用量"
class="inner-cell-input inner-cell-input--usage"
/>
</template>
</el-table-column>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column label="必填" min-width="80" align="center" header-align="center">
<template #default="{ row }">
<el-switch v-model="row.isRequired" />
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button
link
type="danger"
size="small"
@click="consumableList.splice($index, 1)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="consumableList.length === 0"
description="暂无耗材,点击上方按钮添加"
:image-size="48"
/>
<el-table-column prop="consumableCode" label="耗材编码" min-width="110" align="center" header-align="center" />
<el-table-column prop="consumableName" label="耗材名称" min-width="120" align="center" header-align="center" />
<el-table-column prop="specModel" label="规格型号" min-width="110" align="center" header-align="center" />
<el-table-column label="标准用量" min-width="110" align="center" header-align="center">
<template #default="{ row }">
<el-input
v-model="row.standardUsage"
placeholder="用量"
class="inner-cell-input inner-cell-input--usage"
/>
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column label="必填" min-width="80" align="center" header-align="center">
<template #default="{ row }">
<el-switch v-model="row.isRequired" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="consumableList.splice($index, 1)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="consumableList.length === 0" description="暂无耗材,点击上方按钮添加" :image-size="48" />
</div>
</div>
@@ -359,46 +472,84 @@
添加产出物
</el-button>
</div>
<el-table :data="outputList" stripe class="inner-table">
<el-table-column label="产出物类型" min-width="150" align="center" header-align="center">
<template #default="{ row }">
<el-select
v-model="row.outputTypeId"
placeholder="选择类型"
filterable
class="inner-cell-select"
>
<el-option
v-for="ot in outputTypeOptions"
:key="ot.id"
:label="ot.typeName || ot.name"
:value="ot.id"
<el-table :data="outputList" stripe class="inner-table">
<el-table-column
label="产出物类型"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.outputTypeId"
placeholder="选择类型"
filterable
class="inner-cell-select"
>
<el-option
v-for="ot in outputTypeOptions"
:key="ot.id"
:label="ot.typeName || ot.name"
:value="ot.id"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="产出物名称"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.outputName"
placeholder="名称"
class="inner-cell-input"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="产出物名称" min-width="150" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.outputName" placeholder="名称" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="产出物条码" min-width="150" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.outputBarcode" placeholder="条码" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="outputList.splice($index, 1)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="outputList.length === 0" description="暂无产出物,点击上方按钮添加" :image-size="48" />
</template>
</el-table-column>
<el-table-column
label="产出物条码"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.outputBarcode"
placeholder="条码"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button
link
type="danger"
size="small"
@click="outputList.splice($index, 1)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="outputList.length === 0"
description="暂无产出物,点击上方按钮添加"
:image-size="48"
/>
</div>
</div>
@@ -409,76 +560,123 @@
<h3>数据采集</h3>
<span class="inner-table-sort-tip">拖拽左侧手柄可调整排序</span>
</div>
<el-button class="btn-main" type="primary" size="small" @click="addDataCollectionRow">
<el-button
class="btn-main"
type="primary"
size="small"
@click="addDataCollectionRow"
>
<el-icon><Plus /></el-icon>
添加采集项
</el-button>
</div>
<el-table
ref="dataCollectionTableRef"
:data="dataCollectionList"
row-key="_clientKey"
stripe
class="inner-table data-collection-sort-table"
>
<el-table-column label="" min-width="52" align="center" header-align="center" class-name="drag-col">
<template #default>
<el-icon class="data-collection-drag-handle" title="拖拽排序"><Rank /></el-icon>
</template>
</el-table-column>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column label="采集项名称" min-width="180" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.collectionName" placeholder="请输入采集项名称" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="单位" min-width="150" align="center" header-align="center">
<template #default="{ row }">
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="itemUnitOptionsLoading"
:remote-method="searchItemUnitOptions"
@visible-change="(visible) => visible && searchItemUnitOptions('')"
>
<el-option
v-for="item in itemUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
<el-table
ref="dataCollectionTableRef"
:data="dataCollectionList"
row-key="_clientKey"
stripe
class="inner-table data-collection-sort-table"
>
<el-table-column
label=""
min-width="52"
align="center"
header-align="center"
class-name="drag-col"
>
<template #default>
<el-icon class="data-collection-drag-handle" title="拖拽排序"
><Rank
/></el-icon>
</template>
</el-table-column>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column
label="采集项名称"
min-width="180"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input
v-model="row.collectionName"
placeholder="请输入采集项名称"
class="inner-cell-input"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="排序号" min-width="88" align="center" header-align="center">
<template #default="{ $index }">
<span class="data-collection-order-num">{{ $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column label="备注" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="removeDataCollectionRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-if="dataCollectionList.length === 0" description="暂无采集项,点击上方按钮添加" :image-size="48" />
</template>
</el-table-column>
<el-table-column
label="单位"
min-width="150"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-select
v-model="row.unit"
placeholder="请选择单位"
clearable
filterable
remote
reserve-keyword
class="inner-cell-select"
:loading="itemUnitOptionsLoading"
:remote-method="searchItemUnitOptions"
@visible-change="(visible) => visible && searchItemUnitOptions('')"
>
<el-option
v-for="item in itemUnitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="排序号"
min-width="88"
align="center"
header-align="center"
>
<template #default="{ $index }">
<span class="data-collection-order-num">{{ $index + 1 }}</span>
</template>
</el-table-column>
<el-table-column
label="备注"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button
link
type="danger"
size="small"
@click="removeDataCollectionRow($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty
v-if="dataCollectionList.length === 0"
description="暂无采集项,点击上方按钮添加"
:image-size="48"
/>
</div>
</div>
</el-form>
@@ -526,14 +724,65 @@
row-key="id"
@selection-change="handleReagentSelectionChange"
>
<el-table-column type="selection" min-width="48" align="center" header-align="center" :reserve-selection="true" />
<el-table-column prop="reagentCode" label="试剂编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="reagentName" label="试剂名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="spec" label="规格" min-width="100" align="center" header-align="center" />
<el-table-column prop="standardUsage" label="标准用量" min-width="110" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column prop="remark" label="备注" min-width="120" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="description" label="描述" min-width="160" align="center" header-align="center" show-overflow-tooltip />
<el-table-column
type="selection"
min-width="48"
align="center"
header-align="center"
:reserve-selection="true"
/>
<el-table-column
prop="reagentCode"
label="试剂编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="reagentName"
label="试剂名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="spec"
label="规格"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="standardUsage"
label="标准用量"
min-width="110"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
prop="remark"
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="description"
label="描述"
min-width="160"
align="center"
header-align="center"
show-overflow-tooltip
/>
</el-table>
<template #footer>
<el-button class="btn-ghost" @click="selectReagentVisible = false">取消</el-button>
@@ -578,14 +827,65 @@
row-key="id"
@selection-change="handleConsumableSelectionChange"
>
<el-table-column type="selection" min-width="48" align="center" header-align="center" :reserve-selection="true" />
<el-table-column prop="consumableCode" label="耗材编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="consumableName" label="耗材名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="specModel" label="规格型号" min-width="120" align="center" header-align="center" />
<el-table-column prop="standardUsage" label="标准用量" min-width="110" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column prop="remark" label="备注" min-width="120" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="description" label="描述" min-width="160" align="center" header-align="center" show-overflow-tooltip />
<el-table-column
type="selection"
min-width="48"
align="center"
header-align="center"
:reserve-selection="true"
/>
<el-table-column
prop="consumableCode"
label="耗材编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="consumableName"
label="耗材名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="specModel"
label="规格型号"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="standardUsage"
label="标准用量"
min-width="110"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
prop="remark"
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="description"
label="描述"
min-width="160"
align="center"
header-align="center"
show-overflow-tooltip
/>
</el-table>
<template #footer>
<el-button class="btn-ghost" @click="selectConsumableVisible = false">取消</el-button>
@@ -630,9 +930,28 @@
<el-radio v-model="selectedPackId" :value="row.id" @click.stop />
</template>
</el-table-column>
<el-table-column prop="packName" label="耗材包名称" min-width="160" align="center" header-align="center" />
<el-table-column prop="packCode" label="编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="usageScene" label="使用场景" min-width="140" align="center" header-align="center" show-overflow-tooltip />
<el-table-column
prop="packName"
label="耗材包名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="packCode"
label="编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="usageScene"
label="使用场景"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="类型" min-width="130" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="packTypeTagType(row.packType)" size="small">
@@ -647,7 +966,14 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" align="center" header-align="center" show-overflow-tooltip />
<el-table-column
prop="remark"
label="备注"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
/>
</el-table>
<template #footer>
<el-button class="btn-ghost" @click="importPackVisible = false">取消</el-button>
@@ -1001,7 +1327,10 @@
return true
}
function appendConsumableFromMaster(c: any, preset?: { standardUsage?: string; remark?: string }) {
function appendConsumableFromMaster(
c: any,
preset?: { standardUsage?: string; remark?: string }
) {
if (isConsumableDuplicate(c.id || c.consumableId, c.consumableCode)) return false
consumableList.value.push({
consumableId: c.consumableId || c.id,
@@ -1040,10 +1369,7 @@
dialogLoading.value = true
nextTick(() => formRef.value?.clearValidate?.())
try {
const [, detail] = await Promise.all([
loadReferenceData(),
fetchProcessDetail(row.id)
])
const [, detail] = await Promise.all([loadReferenceData(), fetchProcessDetail(row.id)])
formData.processCode = detail.processCode || ''
formData.processName = detail.processName || ''
formData.categoryId = detail.categoryId || ''
@@ -1160,7 +1486,9 @@
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
try {
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个工艺吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个工艺吗?`, '提示', {
type: 'warning'
})
} catch {
return
}
@@ -1451,7 +1779,9 @@
padding: 22px 24px;
margin-bottom: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
transition: transform 0.2s, box-shadow 0.2s;
transition:
transform 0.2s,
box-shadow 0.2s;
position: relative;
background: #fff;
@@ -15,10 +15,20 @@
<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 @keyup.enter="handleSearch" />
<el-input
v-model="searchForm.keyword"
placeholder="任务号/所属工单号/协议号/样本编码"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="任务状态">
<el-select v-model="searchForm.taskStatus" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.taskStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="待配置" value="dpz" />
<el-option label="进行中" value="jxz" />
<el-option label="已完成" value="ywc" />
@@ -37,10 +47,16 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">默认展示近 30 天内已开始的任务;清空日期可查看尚未开始的待配置任务。</span>
<span class="filter-hint"
>默认展示近 30 天内已开始的任务;清空日期可查看尚未开始的待配置任务。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -54,7 +70,13 @@
stripe
@row-dblclick="handleViewDetail"
>
<el-table-column type="index" label="序号" min-width="68" align="center" :index="(i: number) => (pagination.page - 1) * pagination.pageSize + i + 1" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
:index="(i: number) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="taskNo" label="任务号" min-width="150" align="center" />
<el-table-column prop="workOrderNo" label="所属工单号" min-width="140" align="center" />
<el-table-column prop="orderSource" label="来源" min-width="100" align="center">
@@ -72,9 +94,20 @@
<span v-if="shouldShowTempEmptyField(row, row.protocolStatus)">—</span>
<el-tag
v-else
:type="protocolStatusTagTypeBySource(String(row.protocolStatus || ''), row.receptionSource || 'cell_storage')"
:type="
protocolStatusTagTypeBySource(
String(row.protocolStatus || ''),
row.receptionSource || 'cell_storage'
)
"
size="small"
>{{ protocolStatusLabelBySource(String(row.protocolStatus || ''), row.receptionSource || 'cell_storage') }}</el-tag>
>{{
protocolStatusLabelBySource(
String(row.protocolStatus || ''),
row.receptionSource || 'cell_storage'
)
}}</el-tag
>
</template>
</el-table-column>
<el-table-column prop="sampleCode" label="样本编码" min-width="130" align="center">
@@ -85,11 +118,25 @@
</el-table-column>
<el-table-column prop="taskType" label="任务类型" min-width="120" align="center">
<template #default="{ row }">
<el-tag :type="taskTypeTag(row.taskType, row.orderSource).type" size="small">{{ taskTypeTag(row.taskType, row.orderSource).label }}</el-tag>
<el-tag :type="taskTypeTag(row.taskType, row.orderSource).type" size="small">{{
taskTypeTag(row.taskType, row.orderSource).label
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="taskName" label="任务名称" min-width="160" align="center" show-overflow-tooltip />
<el-table-column prop="routeName" label="工艺路线" min-width="140" align="center" show-overflow-tooltip>
<el-table-column
prop="taskName"
label="任务名称"
min-width="160"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="routeName"
label="工艺路线"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.routeName || '—' }}</template>
</el-table-column>
<el-table-column prop="receiverName" label="接收人" min-width="100" align="center">
@@ -100,7 +147,9 @@
</el-table-column>
<el-table-column prop="taskStatus" label="任务状态" min-width="110" align="center">
<template #default="{ row }">
<el-tag :type="taskStatusTag(row.taskStatus).type" size="small">{{ taskStatusTag(row.taskStatus).label }}</el-tag>
<el-tag :type="taskStatusTag(row.taskStatus).type" size="small">{{
taskStatusTag(row.taskStatus).label
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="conclusion" label="生产结论" min-width="100" align="center">
@@ -123,12 +172,21 @@
</el-table-column>
<el-table-column label="操作" min-width="400" fixed="right" align="center">
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEnterExecution(row)"><el-icon><VideoPlay /></el-icon>进入执行</el-button>
<el-button class="btn-op-item primary" @click="handleEnterExecution(row)"
><el-icon><VideoPlay /></el-icon>进入执行</el-button
>
<el-button class="btn-op-item" @click="handleEditTask(row)">
<el-icon><EditPen /></el-icon>编辑
</el-button>
<el-button class="btn-op-item" @click="handleViewDetail(row)"><el-icon><View /></el-icon>查看详情</el-button>
<el-button v-if="row.taskStatus === 'dpz'" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
<el-button class="btn-op-item" @click="handleViewDetail(row)"
><el-icon><View /></el-icon>查看详情</el-button
>
<el-button
v-if="row.taskStatus === 'dpz'"
class="btn-op-item"
@click="handleDelete(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -139,7 +197,9 @@
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val: number) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@size-change="
(val: number) => ((pagination.page = 1), (pagination.pageSize = val), loadData())
"
@current-change="(val: number) => ((pagination.page = val), loadData())"
/>
</div>
@@ -169,7 +229,9 @@
@keyup.enter="searchWorkOrders"
>
<template #append>
<el-button @click="searchWorkOrders"><el-icon><Search /></el-icon></el-button>
<el-button @click="searchWorkOrders"
><el-icon><Search /></el-icon
></el-button>
</template>
</el-input>
</div>
@@ -191,8 +253,14 @@
</el-table-column>
<el-table-column prop="receptionSource" label="来源" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.receptionSource === 'research_donation' ? 'warning' : 'success'" size="small">
{{ row.receptionSourceLabel || (row.receptionSource === 'research_donation' ? '科研捐赠' : '细胞存储') }}
<el-tag
:type="row.receptionSource === 'research_donation' ? 'warning' : 'success'"
size="small"
>
{{
row.receptionSourceLabel ||
(row.receptionSource === 'research_donation' ? '科研捐赠' : '细胞存储')
}}
</el-tag>
</template>
</el-table-column>
@@ -205,11 +273,17 @@
</el-table-column>
<el-table-column label="操作" min-width="100" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" size="small" @click.stop="selectWorkOrder(row)">选择</el-button>
<el-button link type="primary" size="small" @click.stop="selectWorkOrder(row)"
>选择</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!addSelectWoLoading && !addSelectWoList.length" description="暂无可选工单" :image-size="48" />
<el-empty
v-if="!addSelectWoLoading && !addSelectWoList.length"
description="暂无可选工单"
:image-size="48"
/>
</el-dialog>
<DispatchTaskDialog
@@ -229,30 +303,40 @@
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { RefreshLeft, Search, VideoPlay, View, Plus, Close, Check, EditPen } from '@element-plus/icons-vue'
import {
RefreshLeft,
Search,
VideoPlay,
View,
Plus,
Close,
Check,
EditPen
} from '@element-plus/icons-vue'
import { fetchProductionTaskList, fetchDeleteProductionTask } from '@/api/production-task'
import { fetchProductionWorkOrderList } from '@/api/production-work-order'
import {
protocolStatusLabel,
protocolStatusTagType,
donationStatusLabel,
donationStatusTagType,
donationStatusTagType
} from '@/utils/status-tag-registry'
import type { TagType } from '@/utils/status-tag-registry'
import TaskExecutionDialog from '../production-work-order/TaskExecutionDialog.vue'
import DispatchTaskDialog from '../production-work-order/DispatchTaskDialog.vue'
import EditTaskInfoDialog from '../production-work-order/EditTaskInfoDialog.vue'
import {
productionConclusionLabel,
productionConclusionTagType
} from '@/utils/production-labels'
import { productionConclusionLabel, productionConclusionTagType } from '@/utils/production-labels'
/** 根据来源类型返回协议状态标签 */
function protocolStatusLabelBySource(status: string, source: string): string {
return source === 'research_donation' ? donationStatusLabel(status) : protocolStatusLabel(status)
return source === 'research_donation'
? donationStatusLabel(status)
: protocolStatusLabel(status)
}
function protocolStatusTagTypeBySource(status: string, source: string): TagType {
return source === 'research_donation' ? donationStatusTagType(status) : protocolStatusTagType(status)
return source === 'research_donation'
? donationStatusTagType(status)
: protocolStatusTagType(status)
}
/** 临时工单无关联协议/样本时,空字段展示为 — */
@@ -11,7 +11,13 @@
:append-to-body="true"
@close="handleClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="biz-dialog-form">
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
class="biz-dialog-form"
>
<el-form-item label="生产结论" prop="conclusion">
<el-radio-group v-model="form.conclusion">
<el-radio value="cg">成功</el-radio>
@@ -29,7 +35,9 @@
</el-form-item>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="handleClose"><el-icon><Close /></el-icon>取消</el-button>
<el-button class="btn-ghost" @click="handleClose"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit">
<el-icon><Check /></el-icon>{{ isCompletedEdit ? '保存' : '确认完成' }}
</el-button>
@@ -298,7 +298,10 @@
form.routeId = ''
form.description = ''
form.remark = ''
if (props.workOrderRow?.workOrderType === 'temporary' && props.workOrderRow?.workOrderNo) {
if (
props.workOrderRow?.workOrderType === 'temporary' &&
props.workOrderRow?.workOrderNo
) {
form.taskName = `${props.workOrderRow.workOrderNo}任务`
}
loadRouteOptions()
@@ -52,10 +52,7 @@
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import {
fetchProductionTaskDetail,
fetchUpdateProductionTask
} from '@/api/production-task'
import { fetchProductionTaskDetail, fetchUpdateProductionTask } from '@/api/production-task'
import type { FormInstance, FormRules } from 'element-plus'
const props = defineProps<{
@@ -136,7 +136,12 @@
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="试剂编码" min-width="110" align="center" show-overflow-tooltip>
<el-table-column
label="试剂编码"
min-width="110"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input
v-if="!readonly"
@@ -147,7 +152,12 @@
<span v-else>{{ row.reagentCode || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="试剂名称" min-width="160" align="center" show-overflow-tooltip>
<el-table-column
label="试剂名称"
min-width="160"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-select
v-if="!readonly"
@@ -299,7 +309,12 @@
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="耗材编码" min-width="110" align="center" show-overflow-tooltip>
<el-table-column
label="耗材编码"
min-width="110"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input
v-if="!readonly"
@@ -310,7 +325,12 @@
<span v-else>{{ row.consumableCode || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="耗材名称" min-width="160" align="center" show-overflow-tooltip>
<el-table-column
label="耗材名称"
min-width="160"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-select
v-if="!readonly"
@@ -336,7 +356,12 @@
<span v-else>{{ row.consumableName || '—' }}</span>
</template>
</el-table-column>
<el-table-column label="规格型号" min-width="110" align="center" show-overflow-tooltip>
<el-table-column
label="规格型号"
min-width="110"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input
v-if="!readonly"
@@ -457,7 +482,12 @@
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="产出类型" min-width="150" align="center" show-overflow-tooltip>
<el-table-column
label="产出类型"
min-width="150"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-select
v-if="!readonly"
@@ -552,7 +582,12 @@
align="center"
:index="(i: number) => i + 1"
/>
<el-table-column label="采集名称" min-width="180" align="center" show-overflow-tooltip>
<el-table-column
label="采集名称"
min-width="180"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
<el-input
v-if="!readonly"
@@ -1,329 +1,340 @@
<template>
<div class="task-execution-dialog-root">
<el-dialog
v-model="dialogVisible"
:title="readonly ? `任务详情 — ${taskName}` : `任务执行 — ${taskName}`"
width="96vw"
class="dialog-form-layout execution-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
:append-to-body="appendToBody"
@close="handleClose"
>
<!-- 顶部:任务基本信息一行展示 -->
<div v-if="editor.taskDetail.value" class="execution-header">
<div class="execution-header-row">
<span class="execution-header-item"
>任务号:<b>{{ editor.taskDetail.value.taskNo }}</b></span
>
<span class="execution-header-item"
>工单号:{{ editor.taskDetail.value.workOrderNo || '—' }}</span
>
<span class="execution-header-item"
>任务名称:{{ editor.taskDetail.value.taskName || '—' }}</span
>
<el-tag :type="editor.taskStatusTag(editor.taskDetail.value.taskStatus).type">
{{ editor.taskStatusTag(editor.taskDetail.value.taskStatus).label }}
</el-tag>
<span class="execution-header-item execution-header-conclusion">
生产结论:
<el-tag
v-if="productionConclusionTagType(executionTaskConclusion)"
:type="productionConclusionTagType(executionTaskConclusion)"
size="small"
<el-dialog
v-model="dialogVisible"
:title="readonly ? `任务详情 — ${taskName}` : `任务执行 — ${taskName}`"
width="96vw"
class="dialog-form-layout execution-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
:append-to-body="appendToBody"
@close="handleClose"
>
<!-- 顶部:任务基本信息一行展示 -->
<div v-if="editor.taskDetail.value" class="execution-header">
<div class="execution-header-row">
<span class="execution-header-item"
>任务号:<b>{{ editor.taskDetail.value.taskNo }}</b></span
>
{{ productionConclusionLabel(executionTaskConclusion) }}
<span class="execution-header-item"
>工单号:{{ editor.taskDetail.value.workOrderNo || '—' }}</span
>
<span class="execution-header-item"
>任务名称:{{ editor.taskDetail.value.taskName || '—' }}</span
>
<el-tag :type="editor.taskStatusTag(editor.taskDetail.value.taskStatus).type">
{{ editor.taskStatusTag(editor.taskDetail.value.taskStatus).label }}
</el-tag>
<template v-else>—</template>
</span>
<div v-if="!readonly" class="execution-header-actions">
<el-button
v-if="editor.taskDetail.value.taskStatus === 'dpz'"
class="btn-main"
type="primary"
size="small"
:loading="editor.taskActionLoading.value"
@click="editor.handleStartTask"
>
<el-icon><VideoPlay /></el-icon>开始执行
</el-button>
<el-button
v-if="editor.taskDetail.value.taskStatus === 'jxz' || editor.taskDetail.value.taskStatus === 'ywc'"
class="btn-main"
type="success"
size="small"
:loading="editor.taskActionLoading.value"
@click="editor.handleCompleteTask"
>
<el-icon><Check /></el-icon>完成任务
</el-button>
<span class="execution-header-item execution-header-conclusion">
生产结论:
<el-tag
v-if="productionConclusionTagType(executionTaskConclusion)"
:type="productionConclusionTagType(executionTaskConclusion)"
size="small"
>
{{ productionConclusionLabel(executionTaskConclusion) }}
</el-tag>
<template v-else>—</template>
</span>
<div v-if="!readonly" class="execution-header-actions">
<el-button
v-if="editor.taskDetail.value.taskStatus === 'dpz'"
class="btn-main"
type="primary"
size="small"
:loading="editor.taskActionLoading.value"
@click="editor.handleStartTask"
>
<el-icon><VideoPlay /></el-icon>开始执行
</el-button>
<el-button
v-if="
editor.taskDetail.value.taskStatus === 'jxz' ||
editor.taskDetail.value.taskStatus === 'ywc'
"
class="btn-main"
type="success"
size="small"
:loading="editor.taskActionLoading.value"
@click="editor.handleCompleteTask"
>
<el-icon><Check /></el-icon>完成任务
</el-button>
</div>
</div>
<div v-if="editor.taskDetail.value.remark" class="execution-header-remark">{{
editor.taskDetail.value.remark
}}</div>
</div>
<div v-if="editor.taskDetail.value.remark" class="execution-header-remark">{{
editor.taskDetail.value.remark
}}</div>
</div>
<!-- 主体:可折叠工艺库 + 步骤列表 + 详情面板 -->
<div class="execution-main">
<!-- 可折叠工艺库侧栏(只读时隐藏) -->
<template v-if="!readonly">
<transition name="slide-lib">
<div v-show="editor.processLibraryExpanded.value" class="execution-lib-panel">
<div class="execution-lib-header">
<h3>工艺库</h3>
<el-input
v-model="editor.processSearchKeyword.value"
placeholder="搜索工序"
clearable
size="small"
class="route-config-search"
<!-- 主体:可折叠工艺库 + 步骤列表 + 详情面板 -->
<div class="execution-main">
<!-- 可折叠工艺库侧栏(只读时隐藏) -->
<template v-if="!readonly">
<transition name="slide-lib">
<div v-show="editor.processLibraryExpanded.value" class="execution-lib-panel">
<div class="execution-lib-header">
<h3>工艺库</h3>
<el-input
v-model="editor.processSearchKeyword.value"
placeholder="搜索工序"
clearable
size="small"
class="route-config-search"
/>
</div>
<el-collapse v-model="editor.expandedCategories.value" class="route-config-collapse">
<el-collapse-item
v-for="cat in editor.filteredProcessLibrary.value"
:key="cat.categoryId"
:name="cat.categoryId"
>
<template #title>
<span class="route-config-cat-bar"></span>
<span>{{ cat.categoryName }}</span>
</template>
<div
v-for="(proc, procIndex) in cat.processes"
:key="proc.id"
class="route-config-process-card"
draggable="true"
@dragstart="editor.onDragStart($event, proc)"
>
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span
>{{ proc.processName }}
</div>
<div class="route-config-process-code">{{ proc.processCode }}</div>
</div>
<el-icon class="route-config-process-drag"><Rank /></el-icon>
</div>
<el-empty
v-if="!cat.processes.length"
description="该分类暂无工序"
:image-size="32"
/>
</el-collapse-item>
</el-collapse>
<el-empty
v-if="!editor.filteredProcessLibrary.value.length"
description="暂无工艺数据"
:image-size="48"
/>
</div>
<el-collapse v-model="editor.expandedCategories.value" class="route-config-collapse">
<el-collapse-item
v-for="cat in editor.filteredProcessLibrary.value"
:key="cat.categoryId"
:name="cat.categoryId"
>
<template #title>
<span class="route-config-cat-bar"></span>
<span>{{ cat.categoryName }}</span>
</template>
<div
v-for="(proc, procIndex) in cat.processes"
:key="proc.id"
class="route-config-process-card"
draggable="true"
@dragstart="editor.onDragStart($event, proc)"
>
<div class="route-config-process-info">
<div class="route-config-process-name">
<span class="route-config-process-seq">{{ procIndex + 1 }}</span
>{{ proc.processName }}
</div>
<div class="route-config-process-code">{{ proc.processCode }}</div>
</div>
<el-icon class="route-config-process-drag"><Rank /></el-icon>
</div>
<el-empty
v-if="!cat.processes.length"
description="该分类暂无工序"
:image-size="32"
/>
</el-collapse-item>
</el-collapse>
<el-empty
v-if="!editor.filteredProcessLibrary.value.length"
description="暂无工艺数据"
:image-size="48"
/>
</div>
</transition>
</transition>
<!-- 工艺库折叠把手 -->
<!-- 工艺库折叠把手 -->
<div
class="execution-lib-toggle"
@click="editor.processLibraryExpanded.value = !editor.processLibraryExpanded.value"
>
<span>{{ editor.processLibraryExpanded.value ? '‹' : '›' }}</span>
<span class="execution-lib-toggle-text">工艺库</span>
</div>
</template>
<!-- 步骤列表 -->
<div
class="execution-lib-toggle"
@click="editor.processLibraryExpanded.value = !editor.processLibraryExpanded.value"
class="execution-step-list"
v-loading="editor.stepListLoading.value"
@dragover.prevent
@drop="editor.onDropToCenter"
>
<span>{{ editor.processLibraryExpanded.value ? '‹' : '›' }}</span>
<span class="execution-lib-toggle-text">工艺库</span>
</div>
</template>
<!-- 步骤列表 -->
<div
class="execution-step-list"
v-loading="editor.stepListLoading.value"
@dragover.prevent
@drop="editor.onDropToCenter"
>
<div class="execution-step-list-header">
<h3>工序步骤</h3>
<span class="execution-progress-badge"
>{{ editor.completedStepCount.value }}/{{ editor.stepList.value.length }}</span
>
</div>
<div class="execution-step-list-body">
<!-- 虚拟步骤:任务输入 -->
<div
class="virtual-step-card"
:class="{ 'is-active': editor.selectedVirtualStep.value === 'input' }"
@click="editor.selectVirtualStep('input')"
>
<div class="virtual-step-icon virtual-step-icon--input">📥</div>
<div class="virtual-step-info">
<div class="virtual-step-name">原材料(样本/细胞产品)</div>
</div>
<el-tag v-if="editor.virtualInputStatus.value === 'filled'" type="success" size="small"
>已填写 {{ editor.taskInputs.value.length }} 项</el-tag
<div class="execution-step-list-header">
<h3>工序步骤</h3>
<span class="execution-progress-badge"
>{{ editor.completedStepCount.value }}/{{ editor.stepList.value.length }}</span
>
<el-tag v-else type="danger" size="small">未填写</el-tag>
</div>
<!-- 虚拟步骤:任务输出 -->
<div
class="virtual-step-card"
:class="{ 'is-active': editor.selectedVirtualStep.value === 'output' }"
@click="editor.selectVirtualStep('output')"
>
<div class="virtual-step-icon virtual-step-icon--output">📤</div>
<div class="virtual-step-info">
<div class="virtual-step-name">产出(细胞产品)</div>
</div>
<el-tag v-if="editor.virtualOutputStatus.value === 'filled'" type="success" size="small"
>已填写 {{ editor.taskOutputs.value.length }} 项</el-tag
>
<el-tag v-else type="info" size="small">暂无</el-tag>
</div>
<!-- 分割线 -->
<div class="step-divider"><span>工艺步骤</span></div>
<!-- 真实步骤列表 -->
<draggable
v-model="editor.stepList.value"
:handle="readonly ? undefined : '.route-config-step-handle'"
:animation="200"
@end="editor.onStepDragEnd"
>
<div class="execution-step-list-body">
<!-- 虚拟步骤:任务输入 -->
<div
v-for="(element, index) in editor.stepList.value"
:key="element.id"
class="route-config-step-card"
:class="{
'is-active': editor.selectedStepId.value === element.id,
'is-done': element.stepStatus === 'ywc'
}"
@click="editor.selectStep(element)"
class="virtual-step-card"
:class="{ 'is-active': editor.selectedVirtualStep.value === 'input' }"
@click="editor.selectVirtualStep('input')"
>
<div v-if="!readonly" class="route-config-step-handle"
><el-icon><Rank /></el-icon
></div>
<div class="route-config-step-order" :class="stepSeqClass(element.stepStatus)">{{
element.stepStatus === 'ywc' ? '✓' : index + 1
}}</div>
<div class="route-config-step-info">
<div class="route-config-step-name">{{ element.processName || '未命名步骤' }}</div>
<div class="route-config-step-desc">{{ element.description || '' }}</div>
<div class="virtual-step-icon virtual-step-icon--input">📥</div>
<div class="virtual-step-info">
<div class="virtual-step-name">原材料(样本/细胞产品)</div>
</div>
<el-tag
v-if="element.stepStatus"
:type="editor.stepStatusTag(element.stepStatus).type"
v-if="editor.virtualInputStatus.value === 'filled'"
type="success"
size="small"
style="margin-right: 4px"
>{{ editor.stepStatusTag(element.stepStatus).label }}</el-tag
>已填写 {{ editor.taskInputs.value.length }} 项</el-tag
>
<template v-if="!readonly">
<el-button
link
type="primary"
size="small"
title="复制步骤"
@click.stop="editor.handleCopyStep(element)"
>
<el-icon><CopyDocument /></el-icon>
</el-button>
<el-button
link
type="danger"
size="small"
@click.stop="editor.handleDeleteStep(element)"
>
<el-icon><Delete /></el-icon>
</el-button>
</template>
<el-tag v-else type="danger" size="small">未填写</el-tag>
</div>
</draggable>
<div v-if="!editor.stepList.value.length" class="execution-step-list-empty">
<el-icon :size="32" color="#a7f3d0"><Plus /></el-icon>
<p>从左侧拖拽工序到此处,添加任务步骤</p>
<!-- 虚拟步骤:任务输出 -->
<div
class="virtual-step-card"
:class="{ 'is-active': editor.selectedVirtualStep.value === 'output' }"
@click="editor.selectVirtualStep('output')"
>
<div class="virtual-step-icon virtual-step-icon--output">📤</div>
<div class="virtual-step-info">
<div class="virtual-step-name">产出(细胞产品)</div>
</div>
<el-tag
v-if="editor.virtualOutputStatus.value === 'filled'"
type="success"
size="small"
>已填写 {{ editor.taskOutputs.value.length }} 项</el-tag
>
<el-tag v-else type="info" size="small">暂无</el-tag>
</div>
<!-- 分割线 -->
<div class="step-divider"><span>工艺步骤</span></div>
<!-- 真实步骤列表 -->
<draggable
v-model="editor.stepList.value"
:handle="readonly ? undefined : '.route-config-step-handle'"
:animation="200"
@end="editor.onStepDragEnd"
>
<div
v-for="(element, index) in editor.stepList.value"
:key="element.id"
class="route-config-step-card"
:class="{
'is-active': editor.selectedStepId.value === element.id,
'is-done': element.stepStatus === 'ywc'
}"
@click="editor.selectStep(element)"
>
<div v-if="!readonly" class="route-config-step-handle"
><el-icon><Rank /></el-icon
></div>
<div class="route-config-step-order" :class="stepSeqClass(element.stepStatus)">{{
element.stepStatus === 'ywc' ? '✓' : index + 1
}}</div>
<div class="route-config-step-info">
<div class="route-config-step-name">{{
element.processName || '未命名步骤'
}}</div>
<div class="route-config-step-desc">{{ element.description || '' }}</div>
</div>
<el-tag
v-if="element.stepStatus"
:type="editor.stepStatusTag(element.stepStatus).type"
size="small"
style="margin-right: 4px"
>{{ editor.stepStatusTag(element.stepStatus).label }}</el-tag
>
<template v-if="!readonly">
<el-button
link
type="primary"
size="small"
title="复制步骤"
@click.stop="editor.handleCopyStep(element)"
>
<el-icon><CopyDocument /></el-icon>
</el-button>
<el-button
link
type="danger"
size="small"
@click.stop="editor.handleDeleteStep(element)"
>
<el-icon><Delete /></el-icon>
</el-button>
</template>
</div>
</draggable>
<div v-if="!editor.stepList.value.length" class="execution-step-list-empty">
<el-icon :size="32" color="#a7f3d0"><Plus /></el-icon>
<p>从左侧拖拽工序到此处,添加任务步骤</p>
</div>
</div>
</div>
</div>
<!-- 右侧详情面板 -->
<div class="execution-detail-panel">
<!-- 任务输入详情 -->
<template v-if="editor.selectedVirtualStep.value === 'input'">
<div class="execution-detail-header">
<h3>📥 原材料(样本/细胞产品)<span class="virtual-step-required">必填</span></h3>
<div v-if="!readonly" class="execution-detail-actions">
<el-button
class="btn-main"
type="primary"
size="small"
:loading="editor.inputSaving.value"
@click="editor.saveTaskInputs"
><el-icon><Check /></el-icon>保存</el-button
>
<!-- 右侧详情面板 -->
<div class="execution-detail-panel">
<!-- 任务输入详情 -->
<template v-if="editor.selectedVirtualStep.value === 'input'">
<div class="execution-detail-header">
<h3>📥 原材料(样本/细胞产品)<span class="virtual-step-required">必填</span></h3>
<div v-if="!readonly" class="execution-detail-actions">
<el-button
class="btn-main"
type="primary"
size="small"
:loading="editor.inputSaving.value"
@click="editor.saveTaskInputs"
><el-icon><Check /></el-icon>保存</el-button
>
</div>
</div>
</div>
<div class="execution-detail-body">
<TaskInputEnhancedTable
:items="editor.taskInputs.value"
:loading="false"
:disabled="readonly"
:sample-code="editor.taskDetail.value?.sampleCode || ''"
:cell-product-options="editor.cellProductOptions.value"
:cell-product-options-loading="editor.cellProductOptionsLoading.value"
@update:items="(items: Record<string, any>[]) => (editor.taskInputs.value = items)"
@search-cell-product="(q: string) => editor.searchCellProductOptions(q)"
@save="editor.saveTaskInputs"
/>
</div>
</template>
<!-- 任务输出详情 -->
<template v-else-if="editor.selectedVirtualStep.value === 'output'">
<div class="execution-detail-header">
<h3>📤 产出(细胞产品)</h3>
<div v-if="!readonly" class="execution-detail-actions">
<el-button
class="btn-main"
type="primary"
size="small"
:loading="editor.outputSaving.value"
@click="editor.saveTaskOutputs"
><el-icon><Check /></el-icon>保存</el-button
>
<div class="execution-detail-body">
<TaskInputEnhancedTable
:items="editor.taskInputs.value"
:loading="false"
:disabled="readonly"
:sample-code="editor.taskDetail.value?.sampleCode || ''"
:cell-product-options="editor.cellProductOptions.value"
:cell-product-options-loading="editor.cellProductOptionsLoading.value"
@update:items="(items: Record<string, any>[]) => (editor.taskInputs.value = items)"
@search-cell-product="(q: string) => editor.searchCellProductOptions(q)"
@save="editor.saveTaskInputs"
/>
</div>
</div>
<div class="execution-detail-body">
<TaskOutputList
:items="editor.taskOutputs.value"
:loading="false"
:product-type-options="editor.productTypeOptions.value"
:spec-options="editor.specOptions.value"
:storage-slot-options="editor.storageSlotOptions.value"
:unit-options="editor.itemUnitOptions.value"
:unit-options-loading="editor.itemUnitOptionsLoading.value"
:disabled="readonly"
@update:items="(items: Record<string, any>[]) => (editor.taskOutputs.value = items)"
@search-unit="(q: string) => editor.searchItemUnitOptions(q)"
@save="editor.saveTaskOutputs"
/>
</div>
</template>
</template>
<!-- 步骤详情 -->
<template v-else>
<StepDetailPanel :editor="editor" :readonly="readonly" />
</template>
<!-- 任务输出详情 -->
<template v-else-if="editor.selectedVirtualStep.value === 'output'">
<div class="execution-detail-header">
<h3>📤 产出(细胞产品)</h3>
<div v-if="!readonly" class="execution-detail-actions">
<el-button
class="btn-main"
type="primary"
size="small"
:loading="editor.outputSaving.value"
@click="editor.saveTaskOutputs"
><el-icon><Check /></el-icon>保存</el-button
>
</div>
</div>
<div class="execution-detail-body">
<TaskOutputList
:items="editor.taskOutputs.value"
:loading="false"
:product-type-options="editor.productTypeOptions.value"
:spec-options="editor.specOptions.value"
:storage-slot-options="editor.storageSlotOptions.value"
:unit-options="editor.itemUnitOptions.value"
:unit-options-loading="editor.itemUnitOptionsLoading.value"
:disabled="readonly"
@update:items="(items: Record<string, any>[]) => (editor.taskOutputs.value = items)"
@search-unit="(q: string) => editor.searchItemUnitOptions(q)"
@save="editor.saveTaskOutputs"
/>
</div>
</template>
<!-- 步骤详情 -->
<template v-else>
<StepDetailPanel :editor="editor" :readonly="readonly" />
</template>
</div>
</div>
</div>
</el-dialog>
</el-dialog>
<CompleteProductionTaskDialog
v-model:visible="completeConclusionVisible"
:submitting="taskActionLoading"
:is-completed-edit="completeConclusionIsEdit"
:initial-conclusion="completeConclusionInitial.conclusion"
:initial-remark="completeConclusionInitial.remark"
@submit="submitCompleteWithConclusion"
/>
<CompleteProductionTaskDialog
v-model:visible="completeConclusionVisible"
:submitting="taskActionLoading"
:is-completed-edit="completeConclusionIsEdit"
:initial-conclusion="completeConclusionInitial.conclusion"
:initial-remark="completeConclusionInitial.remark"
@submit="submitCompleteWithConclusion"
/>
</div>
</template>
@@ -335,10 +346,7 @@
import TaskInputEnhancedTable from './TaskInputEnhancedTable.vue'
import TaskOutputList from './TaskOutputList.vue'
import CompleteProductionTaskDialog from './CompleteProductionTaskDialog.vue'
import {
productionConclusionLabel,
productionConclusionTagType
} from '@/utils/production-labels'
import { productionConclusionLabel, productionConclusionTagType } from '@/utils/production-labels'
const props = defineProps<{
visible: boolean
@@ -165,12 +165,7 @@
</el-tag>
</template>
</el-table-column>
<el-table-column
label="批次号"
prop="batchNo"
min-width="120"
show-overflow-tooltip
/>
<el-table-column label="批次号" prop="batchNo" min-width="120" show-overflow-tooltip />
<el-table-column label="规格" prop="specName" min-width="100" show-overflow-tooltip />
<el-table-column
label="预计细胞数量(亿)"
@@ -82,7 +82,7 @@
<div class="list-body list-wrap">
<el-table
ref="workOrderListTableRef"
class="list-table"
:class="['list-table', { 'list-table--no-action-col list-table--all-tab': activeTypeTab === 'all' }]"
:data="tableData"
v-loading="loading"
stripe
@@ -96,6 +96,19 @@
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="workOrderNo" label="工单号" min-width="150" align="center" />
<el-table-column
v-if="activeTypeTab === 'outbound'"
prop="workOrderStatus"
label="工单状态"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="woStatusTag(row.workOrderStatus).type" size="small">
{{ woStatusTag(row.workOrderStatus).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column
v-if="activeTypeTab === 'all'"
prop="workOrderType"
@@ -248,7 +261,13 @@
<el-table-column prop="remark" label="备注" min-width="140" align="center">
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
<el-table-column prop="workOrderStatus" label="工单状态" min-width="100" align="center">
<el-table-column
v-if="activeTypeTab !== 'outbound'"
prop="workOrderStatus"
label="工单状态"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="woStatusTag(row.workOrderStatus).type" size="small">
{{ woStatusTag(row.workOrderStatus).label }}
@@ -1059,6 +1078,14 @@
</script>
<style scoped lang="scss">
/* 全部工单 Tab 无操作列,略增高行高便于阅读多列信息 */
:deep(.list-table.list-table--all-tab) {
td.el-table__cell > .cell {
min-height: 36px;
line-height: 36px;
}
}
.work-order-type-tabs {
padding: 0 16px;
margin-bottom: 4px;
@@ -946,7 +946,8 @@ export function useTaskStepEditor() {
if (!taskDetail.value?.id || taskActionLoading.value) return
const isCompleted = taskDetail.value.taskStatus === 'ywc'
completeConclusionIsEdit.value = isCompleted
completeConclusionInitial.conclusion = (taskDetail.value as { conclusion?: string }).conclusion || ''
completeConclusionInitial.conclusion =
(taskDetail.value as { conclusion?: string }).conclusion || ''
completeConclusionInitial.remark = taskDetail.value.remark || ''
if (!isCompleted) {
const ioErr = validateTaskIoRequired()
+153 -76
View File
@@ -82,7 +82,12 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="selection"
min-width="55"
align="center"
header-align="center"
/>
<el-table-column
type="index"
label="序号"
@@ -133,12 +138,22 @@
align="center"
header-align="center"
/>
<el-table-column label="预警库存" min-width="100" align="center" header-align="center">
<el-table-column
label="预警库存"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ row.warningQty ?? 0 }}
</template>
</el-table-column>
<el-table-column label="库存状态" min-width="100" align="center" header-align="center">
<el-table-column
label="库存状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="isStockLow(row) ? 'danger' : 'success'" size="small">
{{ isStockLow(row) ? '库存预警' : '正常' }}
@@ -199,7 +214,9 @@
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@size-change="
(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())
"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
@@ -211,11 +228,7 @@
<el-form :model="allLogFilter" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input
v-model="allLogFilter.keyword"
placeholder="试剂编码/名称"
clearable
/>
<el-input v-model="allLogFilter.keyword" placeholder="试剂编码/名称" clearable />
</el-form-item>
<el-form-item label="变动类型">
<el-select v-model="allLogFilter.changeType" placeholder="全部" clearable>
@@ -266,7 +279,9 @@
min-width="68"
align="center"
header-align="center"
:index="(index) => (allLogPagination.page - 1) * allLogPagination.pageSize + index + 1"
:index="
(index) => (allLogPagination.page - 1) * allLogPagination.pageSize + index + 1
"
/>
<el-table-column
prop="materialCode"
@@ -282,14 +297,24 @@
align="center"
header-align="center"
/>
<el-table-column label="变动类型" min-width="100" align="center" header-align="center">
<el-table-column
label="变动类型"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="getChangeTypeTagType(row)" size="small">
{{ getChangeTypeLabel(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="变动数量" min-width="120" align="center" header-align="center">
<el-table-column
label="变动数量"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<span :class="row.changeQty < 0 ? 'qty-negative' : 'qty-positive'">
{{ row.changeQty < 0 ? row.changeQty : `+${row.changeQty}` }}
@@ -313,10 +338,20 @@
<el-table-column label="工单号" min-width="140" align="center" header-align="center">
<template #default="{ row }">{{ row.taskNo || '-' }}</template>
</el-table-column>
<el-table-column label="步骤名称" min-width="140" align="center" header-align="center">
<el-table-column
label="步骤名称"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stepName || '-' }}</template>
</el-table-column>
<el-table-column label="来源类型" min-width="120" align="center" header-align="center">
<el-table-column
label="来源类型"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="sourceTypeTagType(row.sourceType)" size="small">
{{ sourceTypeLabel(row.sourceType) }}
@@ -326,10 +361,21 @@
<el-table-column label="领用人" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ displayLogReceiver(row) }}</template>
</el-table-column>
<el-table-column label="用途" min-width="120" align="center" header-align="center" show-overflow-tooltip>
<el-table-column
label="用途"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ displayLogPurpose(row) }}</template>
</el-table-column>
<el-table-column label="领用时间" min-width="170" align="center" header-align="center">
<el-table-column
label="领用时间"
min-width="170"
align="center"
header-align="center"
>
<template #default="{ row }">{{ displayLogUseTime(row) }}</template>
</el-table-column>
<el-table-column
@@ -341,7 +387,9 @@
show-overflow-tooltip
/>
<el-table-column label="操作人" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ row.createByName || row.createBy || '-' }}</template>
<template #default="{ row }">{{
row.createByName || row.createBy || '-'
}}</template>
</el-table-column>
<el-table-column
prop="createTime"
@@ -350,7 +398,13 @@
align="center"
header-align="center"
/>
<el-table-column label="操作" min-width="160" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="160"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleAllLogView(row)">
<el-icon><View /></el-icon>查看
@@ -366,7 +420,11 @@
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@size-change="
(val) => ((allLogPagination.page = 1), (allLogPagination.pageSize = val), loadAllLogs())
(val) => (
(allLogPagination.page = 1),
(allLogPagination.pageSize = val),
loadAllLogs()
)
"
@current-change="(val) => ((allLogPagination.page = val), loadAllLogs())"
/>
@@ -418,7 +476,9 @@
</el-select>
</el-form-item>
<el-form-item label="预警库存">
<template #label>预警库存<span class="form-label-tip">低于该值时列表显示库存预警</span></template>
<template #label
>预警库存<span class="form-label-tip">低于该值时列表显示库存预警</span></template
>
<el-input-number
v-model="formData.warningQty"
:min="0"
@@ -438,12 +498,7 @@
<el-input v-model="formData.description" placeholder="请输入描述" />
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="可选"
/>
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
</div>
</el-form>
@@ -488,12 +543,7 @@
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="stockInForm.remark"
type="textarea"
:rows="3"
placeholder="可选"
/>
<el-input v-model="stockInForm.remark" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
@@ -563,20 +613,10 @@
</el-select>
</el-form-item>
<el-form-item label="用途">
<el-input
v-model="stockOutForm.purpose"
type="textarea"
:rows="2"
placeholder="可选"
/>
<el-input v-model="stockOutForm.purpose" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="stockOutForm.remark"
type="textarea"
:rows="2"
placeholder="可选"
/>
<el-input v-model="stockOutForm.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
@@ -688,10 +728,20 @@
align="center"
header-align="center"
/>
<el-table-column label="当前库存" min-width="100" align="center" header-align="center">
<el-table-column
label="当前库存"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stockQty }}</template>
</el-table-column>
<el-table-column label="入库数量" min-width="130" align="center" header-align="center">
<el-table-column
label="入库数量"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input-number
v-model="row.qty"
@@ -705,22 +755,28 @@
</el-table-column>
<el-table-column label="备注" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="可选" clearable class="inner-cell-input" />
<el-input
v-model="row.remark"
placeholder="可选"
clearable
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="batchStockInLines.splice($index, 1)">
<el-button
link
type="danger"
size="small"
@click="batchStockInLines.splice($index, 1)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="请通过上方搜索框添加要入库的试剂"
:image-size="56"
/>
<el-empty v-else description="请通过上方搜索框添加要入库的试剂" :image-size="56" />
</div>
</el-form>
<template #footer>
@@ -861,10 +917,20 @@
align="center"
header-align="center"
/>
<el-table-column label="当前库存" min-width="100" align="center" header-align="center">
<el-table-column
label="当前库存"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stockQty }}</template>
</el-table-column>
<el-table-column label="出库数量" min-width="130" align="center" header-align="center">
<el-table-column
label="出库数量"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input-number
v-model="row.qty"
@@ -879,22 +945,28 @@
</el-table-column>
<el-table-column label="备注" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="可选" clearable class="inner-cell-input" />
<el-input
v-model="row.remark"
placeholder="可选"
clearable
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="batchStockOutLines.splice($index, 1)">
<el-button
link
type="danger"
size="small"
@click="batchStockOutLines.splice($index, 1)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="请通过上方搜索框添加要出库的试剂"
:image-size="56"
/>
<el-empty v-else description="请通过上方搜索框添加要出库的试剂" :image-size="56" />
</div>
</el-form>
<template #footer>
@@ -927,33 +999,32 @@
<div class="filter-block" style="padding-bottom: 0">
<el-form :inline="true" @submit.prevent>
<el-form-item label="变动类型">
<el-select v-model="stockLogFilter.changeType" placeholder="全部" clearable @change="loadStockLogs">
<el-select
v-model="stockLogFilter.changeType"
placeholder="全部"
clearable
@change="loadStockLogs"
>
<el-option label="入库" value="in" />
<el-option label="出库" value="out" />
</el-select>
</el-form-item>
</el-form>
</div>
<el-table
class="list-table"
:data="stockLogData"
v-loading="stockLogLoading"
stripe
>
<el-table class="list-table" :data="stockLogData" v-loading="stockLogLoading" stripe>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
:index="(index) => (stockLogPagination.page - 1) * stockLogPagination.pageSize + index + 1"
:index="
(index) => (stockLogPagination.page - 1) * stockLogPagination.pageSize + index + 1
"
/>
<el-table-column label="变动类型" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag
:type="row.changeType === 'in' ? 'success' : 'warning'"
size="small"
>
<el-tag :type="row.changeType === 'in' ? 'success' : 'warning'" size="small">
{{ row.changeType === 'in' ? '入库' : '出库' }}
</el-tag>
</template>
@@ -1038,7 +1109,11 @@
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="
(val) => ((stockLogPagination.page = 1), (stockLogPagination.pageSize = val), loadStockLogs())
(val) => (
(stockLogPagination.page = 1),
(stockLogPagination.pageSize = val),
loadStockLogs()
)
"
@current-change="(val) => ((stockLogPagination.page = val), loadStockLogs())"
/>
@@ -1528,7 +1603,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个试剂吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个试剂吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteReagent(id)
+74 -23
View File
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编号、名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -68,17 +72,54 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="sopCode" label="SOP编号" min-width="130" align="center" header-align="center" />
<el-table-column prop="sopName" label="SOP文档名称" min-width="180" align="center" header-align="center" />
<el-table-column prop="versionNo" label="版本号" min-width="100" align="center" header-align="center" />
<el-table-column prop="remark" label="备注" min-width="160" align="center" header-align="center" show-overflow-tooltip />
<el-table-column
prop="sopCode"
label="SOP编号"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="sopName"
label="SOP文档名称"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
prop="versionNo"
label="版本号"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="remark"
label="备注"
min-width="160"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="附件数" min-width="90" align="center" header-align="center">
<template #default="{ row }">
{{ row.sopAttachments ? row.sopAttachments.split(',').length : 0 }}
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="180" align="center" header-align="center" />
<el-table-column label="操作" min-width="320" fixed="right" align="center" header-align="center">
<el-table-column
prop="createTime"
label="创建时间"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -177,7 +218,9 @@
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
<span class="attachment-name" :title="file.fileName">{{
file.fileName
}}</span>
<div class="attachment-actions">
<el-icon
class="action-btn"
@@ -195,9 +238,7 @@
</div>
</div>
</div>
<span class="upload-hint"
>支持图片、视频、PDF、Word、Excel等文件</span
>
<span class="upload-hint">支持图片、视频、PDF、Word、Excel等文件</span>
</div>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
@@ -304,15 +345,23 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, ScaleToOriginal, Document, Download, Delete, VideoPlay, Loading, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
ScaleToOriginal,
Document,
Download,
Delete,
VideoPlay,
Loading,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchSopList,
fetchCreateSop,
fetchUpdateSop,
fetchDeleteSop
} from '@/api/production'
import { fetchSopList, fetchCreateSop, fetchUpdateSop, fetchDeleteSop } from '@/api/production'
import { fetchUploadAttachment } from '@/api/business'
import { downloadAttachmentFile } from '@/utils/download-attachment'
@@ -441,7 +490,9 @@
const viewSections = computed(() => {
if (!viewRow.value) return []
const row = viewRow.value
const attachCount = row.sopAttachments ? row.sopAttachments.split(',').filter(Boolean).length : 0
const attachCount = row.sopAttachments
? row.sopAttachments.split(',').filter(Boolean).length
: 0
return [
{
title: '基本信息',
@@ -460,8 +511,6 @@
const file = uploadFile?.raw || uploadFile
if (!file) return
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
formData.attachments.push({
id: tempId,
@@ -621,7 +670,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个SOP文档吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个SOP文档吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteSop(id)
@@ -19,7 +19,12 @@
<el-input v-model="searchForm.keyword" placeholder="编码/名称" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.status"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="启用" value="1" />
<el-option label="停用" value="0" />
</el-select>
@@ -27,8 +32,12 @@
</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>
<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>
@@ -36,8 +45,12 @@
<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>
<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
>
</div>
</div>
@@ -52,20 +65,40 @@
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" :index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="itemCode" label="项目编码" min-width="140" align="center" />
<el-table-column prop="itemName" label="项目名称" min-width="160" align="center" />
<el-table-column prop="description" label="描述" min-width="200" align="center" show-overflow-tooltip />
<el-table-column
prop="description"
label="描述"
min-width="200"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="status" label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{ row.status === '1' ? '启用' : '停用' }}</el-tag>
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{
row.status === '1' ? '启用' : '停用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="320" fixed="right" align="center">
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item primary" @click="handleEdit(row)"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)"><el-icon><Delete /></el-icon>删除</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="handleEdit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDelete(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -82,8 +115,22 @@
</div>
</div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="840px" class="dialog-form-layout" align-center destroy-on-close :close-on-click-modal="false">
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" class="biz-dialog-form">
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="840px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="biz-dialog-form"
>
<div class="form-grid-3">
<el-form-item label="项目编码" prop="itemCode">
<el-input v-model="formData.itemCode" :disabled="isEdit" />
@@ -106,54 +153,65 @@
<el-input v-model="formData.remark" type="textarea" :rows="2" />
</el-form-item>
</div>
<!-- 参数配置 -->
<div class="form-section" style="margin-top: 8px">
<div class="section-title">参数配置</div>
<div class="param-row param-row-header">
<span style="flex: 2">参数名称</span>
<span style="flex: 1.5">参数编码</span>
<span style="flex: 1">单位</span>
<span style="flex: 1">类型</span>
<span style="width: 40px; text-align: center">必填</span>
<span style="width: 32px"></span>
</div>
<div v-if="itemParams.length === 0" style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px">
暂无参数,点击下方按钮添加
</div>
<div v-for="(param, idx) in itemParams" :key="idx" class="param-row">
<el-input v-model="param.paramName" placeholder="参数名称" style="flex: 2" />
<el-input v-model="param.paramCode" placeholder="参数编码" style="flex: 1.5" />
<el-select
v-model="param.paramUnit"
placeholder="单位"
clearable
filterable
remote
reserve-keyword
:loading="unitOptionsLoading"
:remote-method="searchUnitOptions"
style="flex: 1"
>
<el-option
v-for="item in unitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select v-model="param.paramType" style="flex: 1">
<el-option label="文本" value="text" />
<el-option label="数字" value="number" />
</el-select>
<el-switch v-model="param.requiredFlag" active-value="1" inactive-value="0" />
<el-button class="btn-op-item" @click="removeParam(idx)"><el-icon><Delete /></el-icon></el-button>
</div>
<el-button type="primary" plain style="margin-top: 8px" @click="addParam"><el-icon><Plus /></el-icon>添加参数</el-button>
<!-- 参数配置 -->
<div class="form-section" style="margin-top: 8px">
<div class="section-title">参数配置</div>
<div class="param-row param-row-header">
<span style="flex: 2">参数名称</span>
<span style="flex: 1.5">参数编码</span>
<span style="flex: 1">单位</span>
<span style="flex: 1">类型</span>
<span style="width: 40px; text-align: center">必填</span>
<span style="width: 32px"></span>
</div>
<div
v-if="itemParams.length === 0"
style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px"
>
暂无参数,点击下方按钮添加
</div>
<div v-for="(param, idx) in itemParams" :key="idx" class="param-row">
<el-input v-model="param.paramName" placeholder="参数名称" style="flex: 2" />
<el-input v-model="param.paramCode" placeholder="参数编码" style="flex: 1.5" />
<el-select
v-model="param.paramUnit"
placeholder="单位"
clearable
filterable
remote
reserve-keyword
:loading="unitOptionsLoading"
:remote-method="searchUnitOptions"
style="flex: 1"
>
<el-option
v-for="item in unitOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-select v-model="param.paramType" style="flex: 1">
<el-option label="文本" value="text" />
<el-option label="数字" value="number" />
</el-select>
<el-switch v-model="param.requiredFlag" active-value="1" inactive-value="0" />
<el-button class="btn-op-item" @click="removeParam(idx)"
><el-icon><Delete /></el-icon
></el-button>
</div>
<el-button type="primary" plain style="margin-top: 8px" @click="addParam"
><el-icon><Plus /></el-icon>添加参数</el-button
>
</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" :loading="submitLoading" @click="handleSubmit"><el-icon><Check /></el-icon>确定</el-button>
<el-button class="btn-ghost" @click="dialogVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit"
><el-icon><Check /></el-icon>确定</el-button
>
</template>
</el-dialog>
@@ -171,8 +229,20 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, Search, RefreshLeft, View, Edit, Delete, Notebook } from '@element-plus/icons-vue'
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
import {
Plus,
Close,
Check,
Search,
RefreshLeft,
View,
Edit,
Delete,
Notebook
} from '@element-plus/icons-vue'
import SystemViewDialog, {
type ViewSection
} from '@/components/business/system-view-dialog/index.vue'
import {
fetchQcLabTestItemList,
fetchCreateQcLabTestItem,
@@ -233,7 +303,13 @@
const formRef = ref()
const editingId = ref<string | null>(null)
const itemParams = ref<QcItemParam[]>([])
const formData = reactive({ itemCode: '', itemName: '', description: '', status: '1', remark: '' })
const formData = reactive({
itemCode: '',
itemName: '',
description: '',
status: '1',
remark: ''
})
const viewVisible = ref(false)
const viewItemName = ref('')
const viewItemCode = ref('')
@@ -285,7 +361,13 @@
loadData()
}
function resetForm() {
Object.assign(formData, { itemCode: '', itemName: '', description: '', status: '1', remark: '' })
Object.assign(formData, {
itemCode: '',
itemName: '',
description: '',
status: '1',
remark: ''
})
editingId.value = null
itemParams.value = []
}
@@ -324,7 +406,11 @@
if (!(await formRef.value?.validate?.().catch(() => false))) return
submitLoading.value = true
try {
const payload = { ...formData, remark: formData.remark || undefined, description: formData.description || undefined }
const payload = {
...formData,
remark: formData.remark || undefined,
description: formData.description || undefined
}
let savedId = editingId.value
if (isEdit.value && editingId.value) {
await fetchUpdateQcLabTestItem(editingId.value, payload)
@@ -350,7 +436,11 @@
{
title: '基本信息',
fields: [
{ label: '状态', value: row.status === '1' ? '启用' : '停用', tagType: row.status === '1' ? 'success' : 'info' }
{
label: '状态',
value: row.status === '1' ? '启用' : '停用',
tagType: row.status === '1' ? 'success' : 'info'
}
]
},
{
@@ -4,7 +4,9 @@
<div class="page-container">
<header class="page-head">
<h1>质检物料变动记录</h1>
<p class="page-desc">查看检测物料的使用与入库变动流水,支持按物料、变动类型和时间范围筛选。</p>
<p class="page-desc"
>查看检测物料的使用与入库变动流水,支持按物料、变动类型和时间范围筛选。</p
>
</header>
<section class="panel">
@@ -121,12 +123,7 @@
</span>
</template>
</el-table-column>
<el-table-column
label="变动前库存"
min-width="120"
align="center"
header-align="center"
>
<el-table-column label="变动前库存" min-width="120" align="center" header-align="center">
<template #default="{ row }">
{{ displayStockBefore(row) }}
</template>
@@ -167,7 +164,13 @@
{{ displayReceiver(row) }}
</template>
</el-table-column>
<el-table-column label="用途" min-width="120" align="center" header-align="center" show-overflow-tooltip>
<el-table-column
label="用途"
min-width="120"
align="center"
header-align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ displayPurpose(row) }}
</template>
@@ -203,7 +206,13 @@
align="center"
header-align="center"
/>
<el-table-column label="操作" min-width="160" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="160"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
@@ -238,188 +247,191 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { Search, RefreshLeft, View, Document } from '@element-plus/icons-vue'
import { fetchQcMaterialUsageList } from '@/api/qc-material-usage'
import type { QcMaterialUsage } from '@/api/qc-material-usage'
import { fetchQcMaterialOptions } from '@/api/qc-material'
import type { QcMaterialOption } from '@/api/qc-material'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { Search, RefreshLeft, View, Document } from '@element-plus/icons-vue'
import { fetchQcMaterialUsageList } from '@/api/qc-material-usage'
import type { QcMaterialUsage } from '@/api/qc-material-usage'
import { fetchQcMaterialOptions } from '@/api/qc-material'
import type { QcMaterialOption } from '@/api/qc-material'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
defineOptions({ name: 'QualityInspectionQcMaterialUsage' })
defineOptions({ name: 'QualityInspectionQcMaterialUsage' })
const route = useRoute()
const route = useRoute()
const searchForm = reactive<{
materialId: string
changeType: string
}>({
materialId: '',
changeType: ''
})
const searchForm = reactive<{
materialId: string
changeType: string
}>({
materialId: '',
changeType: ''
})
const dateRange = ref<[string, string] | null>(null)
const tableData = ref<QcMaterialUsage[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const materialOptions = ref<QcMaterialOption[]>([])
const dateRange = ref<[string, string] | null>(null)
const tableData = ref<QcMaterialUsage[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const materialOptions = ref<QcMaterialOption[]>([])
function isManualOutRow(row: QcMaterialUsage) {
if (row.sourceType === 'manual_out') return true
const remark = row.remark || ''
return row.changeType === 'use' && (row.changeQty ?? 0) < 0 && remark.includes('领用人:')
}
function parseFromRemark(remark: string, key: 'receiver' | 'purpose') {
const prefix = key === 'receiver' ? '领用人:' : '用途:'
for (const part of remark.split(';')) {
const seg = part.trim()
if (seg.startsWith(prefix)) return seg.slice(prefix.length).trim()
function isManualOutRow(row: QcMaterialUsage) {
if (row.sourceType === 'manual_out') return true
const remark = row.remark || ''
return row.changeType === 'use' && (row.changeQty ?? 0) < 0 && remark.includes('领用人:')
}
return ''
}
function displayReceiver(row: QcMaterialUsage) {
if (!isManualOutRow(row)) return '-'
return (row.receiver || '').trim() || parseFromRemark(row.remark || '', 'receiver') || '-'
}
function displayPurpose(row: QcMaterialUsage) {
if (!isManualOutRow(row)) return '-'
return (row.purpose || '').trim() || parseFromRemark(row.remark || '', 'purpose') || '-'
}
function displayUseTime(row: QcMaterialUsage) {
if (!isManualOutRow(row)) return '-'
return (row.useTime || row.createTime || '').trim() || '-'
}
function displayStockBefore(row: QcMaterialUsage) {
const after = Number(row.stockAfter ?? 0)
const qty = Number(row.changeQty ?? 0)
return after - qty
}
function getChangeTypeLabel(row: QcMaterialUsage) {
if (row.changeType === 'in') return '入库'
if (isManualOutRow(row)) return '出库'
return '任务使用'
}
function getChangeTypeTagType(row: QcMaterialUsage) {
if (row.changeType === 'in') return 'success'
if (isManualOutRow(row)) return 'warning'
return 'primary'
}
function getSourceTagType(sourceType: string) {
if (sourceType === 'task_complete') return 'primary'
if (sourceType === 'manual_in') return 'success'
if (sourceType === 'manual_out') return 'warning'
return 'info'
}
function getSourceLabel(sourceType: string) {
if (sourceType === 'task_complete') return '任务完成'
if (sourceType === 'manual_in') return '手动入库'
if (sourceType === 'manual_out') return '手动出库'
return sourceType || '-'
}
async function loadMaterialOptions() {
try {
const res = await fetchQcMaterialOptions()
materialOptions.value = res || []
} catch {
materialOptions.value = []
function parseFromRemark(remark: string, key: 'receiver' | 'purpose') {
const prefix = key === 'receiver' ? '领用人:' : '用途:'
for (const part of remark.split(';')) {
const seg = part.trim()
if (seg.startsWith(prefix)) return seg.slice(prefix.length).trim()
}
return ''
}
}
async function loadData() {
loading.value = true
try {
const params: Record<string, any> = {
page: pagination.page,
pageSize: pagination.pageSize
}
if (searchForm.materialId) params.materialId = searchForm.materialId
if (searchForm.changeType) params.changeType = searchForm.changeType
if (dateRange.value && dateRange.value[0]) {
params.startTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
const res = await fetchQcMaterialUsageList(params)
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
function displayReceiver(row: QcMaterialUsage) {
if (!isManualOutRow(row)) return '-'
return (row.receiver || '').trim() || parseFromRemark(row.remark || '', 'receiver') || '-'
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function displayPurpose(row: QcMaterialUsage) {
if (!isManualOutRow(row)) return '-'
return (row.purpose || '').trim() || parseFromRemark(row.remark || '', 'purpose') || '-'
}
function handleReset() {
searchForm.materialId = ''
searchForm.changeType = ''
dateRange.value = null
pagination.page = 1
loadData()
}
function displayUseTime(row: QcMaterialUsage) {
if (!isManualOutRow(row)) return '-'
return (row.useTime || row.createTime || '').trim() || '-'
}
const viewVisible = ref(false)
const viewRow = ref<QcMaterialUsage | null>(null)
function displayStockBefore(row: QcMaterialUsage) {
const after = Number(row.stockAfter ?? 0)
const qty = Number(row.changeQty ?? 0)
return after - qty
}
const viewSections = computed(() => {
if (!viewRow.value) return []
const row = viewRow.value
return [
{
title: '变动信息',
fields: [
{ label: '物料编码', value: row.materialCode },
{ label: '物料名称', value: row.materialName },
{
label: '变动类型',
value: getChangeTypeLabel(row),
tagType: getChangeTypeTagType(row) as any
},
{ label: '变动数量', value: row.changeQty < 0 ? String(row.changeQty) : `+${row.changeQty}` },
{ label: '变动前库存', value: String(displayStockBefore(row)) },
{ label: '变动后库存', value: String(row.stockAfter ?? 0) },
{ label: '关联任务号', value: row.qcTaskNo || '-' },
{
label: '来源类型',
value: getSourceLabel(row.sourceType),
tagType: getSourceTagType(row.sourceType) as any
},
{
label: '领用人',
value: displayReceiver(row) === '-' ? '—' : displayReceiver(row)
},
{
label: '用途',
value: displayPurpose(row) === '-' ? '—' : displayPurpose(row)
},
{
label: '领用时间',
value: displayUseTime(row) === '-' ? '—' : displayUseTime(row)
},
{ label: '备注', value: row.remark || '—', span: true },
{ label: '操作人', value: row.createByName || row.createBy || '-' },
{ label: '变动时间', value: row.createTime || '—' }
]
function getChangeTypeLabel(row: QcMaterialUsage) {
if (row.changeType === 'in') return '入库'
if (isManualOutRow(row)) return '出库'
return '任务使用'
}
function getChangeTypeTagType(row: QcMaterialUsage) {
if (row.changeType === 'in') return 'success'
if (isManualOutRow(row)) return 'warning'
return 'primary'
}
function getSourceTagType(sourceType: string) {
if (sourceType === 'task_complete') return 'primary'
if (sourceType === 'manual_in') return 'success'
if (sourceType === 'manual_out') return 'warning'
return 'info'
}
function getSourceLabel(sourceType: string) {
if (sourceType === 'task_complete') return '任务完成'
if (sourceType === 'manual_in') return '手动入库'
if (sourceType === 'manual_out') return '手动出库'
return sourceType || '-'
}
async function loadMaterialOptions() {
try {
const res = await fetchQcMaterialOptions()
materialOptions.value = res || []
} catch {
materialOptions.value = []
}
]
})
}
function handleView(row: QcMaterialUsage) {
viewRow.value = row
viewVisible.value = true
}
async function loadData() {
loading.value = true
try {
const params: Record<string, any> = {
page: pagination.page,
pageSize: pagination.pageSize
}
if (searchForm.materialId) params.materialId = searchForm.materialId
if (searchForm.changeType) params.changeType = searchForm.changeType
if (dateRange.value && dateRange.value[0]) {
params.startTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
const res = await fetchQcMaterialUsageList(params)
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.materialId = ''
searchForm.changeType = ''
dateRange.value = null
pagination.page = 1
loadData()
}
const viewVisible = ref(false)
const viewRow = ref<QcMaterialUsage | null>(null)
const viewSections = computed(() => {
if (!viewRow.value) return []
const row = viewRow.value
return [
{
title: '变动信息',
fields: [
{ label: '物料编码', value: row.materialCode },
{ label: '物料名称', value: row.materialName },
{
label: '变动类型',
value: getChangeTypeLabel(row),
tagType: getChangeTypeTagType(row) as any
},
{
label: '变动数量',
value: row.changeQty < 0 ? String(row.changeQty) : `+${row.changeQty}`
},
{ label: '变动前库存', value: String(displayStockBefore(row)) },
{ label: '变动后库存', value: String(row.stockAfter ?? 0) },
{ label: '关联任务号', value: row.qcTaskNo || '-' },
{
label: '来源类型',
value: getSourceLabel(row.sourceType),
tagType: getSourceTagType(row.sourceType) as any
},
{
label: '领用人',
value: displayReceiver(row) === '-' ? '—' : displayReceiver(row)
},
{
label: '用途',
value: displayPurpose(row) === '-' ? '—' : displayPurpose(row)
},
{
label: '领用时间',
value: displayUseTime(row) === '-' ? '—' : displayUseTime(row)
},
{ label: '备注', value: row.remark || '—', span: true },
{ label: '操作人', value: row.createByName || row.createBy || '-' },
{ label: '变动时间', value: row.createTime || '—' }
]
}
]
})
function handleView(row: QcMaterialUsage) {
viewRow.value = row
viewVisible.value = true
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
searchForm,
@@ -431,31 +443,28 @@ function handleView(row: QcMaterialUsage) {
)
// 日期范围变更时自动搜索
watch(
dateRange,
() => {
pagination.page = 1
loadData()
}
)
watch(dateRange, () => {
pagination.page = 1
loadData()
})
onMounted(async () => {
await loadMaterialOptions()
if (route.query.materialId) {
searchForm.materialId = route.query.materialId as string
}
loadData()
})
onMounted(async () => {
await loadMaterialOptions()
if (route.query.materialId) {
searchForm.materialId = route.query.materialId as string
}
loadData()
})
</script>
<style scoped lang="scss">
.qty-positive {
color: var(--el-color-success);
font-weight: 500;
}
.qty-positive {
color: var(--el-color-success);
font-weight: 500;
}
.qty-negative {
color: var(--el-color-danger);
font-weight: 500;
}
.qty-negative {
color: var(--el-color-danger);
font-weight: 500;
}
</style>
@@ -4,7 +4,9 @@
<div class="page-container">
<header class="page-head">
<h1>质检物料库</h1>
<p class="page-desc">管理质检物料信息,支持入库、出库操作;质检任务完成时也会自动扣减库存。</p>
<p class="page-desc"
>管理质检物料信息,支持入库、出库操作;质检任务完成时也会自动扣减库存。</p
>
</header>
<section class="panel">
@@ -28,8 +30,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按编码、名称模糊查询,可按状态筛选。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -137,15 +143,29 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" min-width="480" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="480"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleStockIn(row)"><el-icon><Upload /></el-icon>入库</el-button>
<el-button class="btn-op-item" @click="handleStockOut(row)"><el-icon><Download /></el-icon>出库</el-button>
<el-button class="btn-op-item" @click="handleStockIn(row)"
><el-icon><Upload /></el-icon>入库</el-button
>
<el-button class="btn-op-item" @click="handleStockOut(row)"
><el-icon><Download /></el-icon>出库</el-button
>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="handleEdit(row)"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)"><el-icon><Delete /></el-icon>删除</el-button>
<el-button class="btn-op-item primary" @click="handleEdit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDelete(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -211,7 +231,9 @@
</el-select>
</el-form-item>
<el-form-item label="预警库存">
<template #label>预警库存<span class="form-label-tip">低于该值时列表显示库存预警</span></template>
<template #label
>预警库存<span class="form-label-tip">低于该值时列表显示库存预警</span></template
>
<el-input-number
v-model="formData.warningQty"
:min="0"
@@ -228,12 +250,7 @@
</el-radio-group>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="可选"
/>
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
</div>
</el-form>
@@ -287,12 +304,7 @@
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="stockInForm.remark"
type="textarea"
:rows="3"
placeholder="可选"
/>
<el-input v-model="stockInForm.remark" type="textarea" :rows="3" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
@@ -371,20 +383,10 @@
</el-select>
</el-form-item>
<el-form-item label="用途">
<el-input
v-model="stockOutForm.purpose"
type="textarea"
:rows="2"
placeholder="可选"
/>
<el-input v-model="stockOutForm.purpose" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="stockOutForm.remark"
type="textarea"
:rows="2"
placeholder="可选"
/>
<el-input v-model="stockOutForm.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</el-form>
<template #footer>
@@ -485,13 +487,41 @@
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column prop="materialCode" label="物料编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="materialName" label="物料名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column label="当前库存" min-width="100" align="center" header-align="center">
<el-table-column
prop="materialCode"
label="物料编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="物料名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
label="当前库存"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stockQty }}</template>
</el-table-column>
<el-table-column label="入库数量" min-width="130" align="center" header-align="center">
<el-table-column
label="入库数量"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input-number
v-model="row.qty"
@@ -505,22 +535,28 @@
</el-table-column>
<el-table-column label="备注" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="可选" clearable class="inner-cell-input" />
<el-input
v-model="row.remark"
placeholder="可选"
clearable
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="batchStockInLines.splice($index, 1)">
<el-button
link
type="danger"
size="small"
@click="batchStockInLines.splice($index, 1)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="请通过上方搜索框添加要入库的物料"
:image-size="56"
/>
<el-empty v-else description="请通过上方搜索框添加要入库的物料" :image-size="56" />
</div>
</el-form>
<template #footer>
@@ -649,13 +685,41 @@
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column prop="materialCode" label="物料编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="materialName" label="物料名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
<el-table-column label="当前库存" min-width="100" align="center" header-align="center">
<el-table-column
prop="materialCode"
label="物料编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="物料名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
label="当前库存"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.stockQty }}</template>
</el-table-column>
<el-table-column label="出库数量" min-width="130" align="center" header-align="center">
<el-table-column
label="出库数量"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input-number
v-model="row.qty"
@@ -670,22 +734,28 @@
</el-table-column>
<el-table-column label="备注" min-width="160" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="可选" clearable class="inner-cell-input" />
<el-input
v-model="row.remark"
placeholder="可选"
clearable
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="batchStockOutLines.splice($index, 1)">
<el-button
link
type="danger"
size="small"
@click="batchStockOutLines.splice($index, 1)"
>
移除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="请通过上方搜索框添加要出库的物料"
:image-size="56"
/>
<el-empty v-else description="请通过上方搜索框添加要出库的物料" :image-size="56" />
</div>
</el-form>
<template #footer>
@@ -720,7 +790,19 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Box, RefreshLeft, Search, Upload, Download, Edit, Delete } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
Box,
RefreshLeft,
Search,
Upload,
Download,
Edit,
Delete
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { fetchItemUnitList, fetchUserList } from '@/api/admin'
import {
@@ -4,7 +4,9 @@
<div class="page-container">
<header class="page-head">
<h1>质检任务</h1>
<p class="page-desc">按检测项目跟进任务;实验室自检需开始后再填写结果,第三方在填写结果时一并上传检测材料。</p>
<p class="page-desc"
>按检测项目跟进任务;实验室自检需开始后再填写结果,第三方在填写结果时一并上传检测材料。</p
>
</header>
<section class="panel">
@@ -23,20 +25,35 @@
/>
</el-form-item>
<el-form-item label="业务类型">
<el-select v-model="searchForm.qcBizType" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.qcBizType"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="入库质检" value="inbound" />
<el-option label="出库质检" value="outbound" />
<el-option label="临时质检" value="temporary" />
</el-select>
</el-form-item>
<el-form-item label="任务类型">
<el-select v-model="searchForm.taskType" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.taskType"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="实验室自检" value="sysz" />
<el-option label="第三方检测" value="dsfjc" />
</el-select>
</el-form-item>
<el-form-item label="任务状态">
<el-select v-model="searchForm.taskStatus" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.taskStatus"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="待执行" value="dzx" />
<el-option label="进行中" value="jxz" />
<el-option label="已完成" value="ywc" />
@@ -45,16 +62,32 @@
</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>
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
>
<el-button class="btn-main" type="primary" @click="handleSearch"
><el-icon><Search /></el-icon>搜索</el-button
>
</div>
</div>
</el-form>
</div>
<div class="list-body list-wrap">
<el-table class="list-table" :data="tableData" v-loading="loading" stripe @row-dblclick="handleView">
<el-table-column type="index" label="序号" min-width="68" align="center" :index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1" />
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
@row-dblclick="handleView"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="taskNo" label="任务号" min-width="150" align="center" />
<el-table-column prop="qcBizTypeLabel" label="业务类型" min-width="100" align="center">
<template #default="{ row }">
@@ -71,7 +104,12 @@
</el-tag>
</template>
</el-table-column>
<el-table-column prop="receptionSourceLabel" label="样本来源" min-width="100" align="center">
<el-table-column
prop="receptionSourceLabel"
label="样本来源"
min-width="100"
align="center"
>
<template #default="{ row }">
<span v-if="shouldShowTempEmptyReceptionSource(row)">—</span>
<el-tag
@@ -97,7 +135,9 @@
</el-table-column>
<el-table-column prop="taskType" label="检测类型" min-width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.taskType === 'sysz' ? 'success' : 'info'" size="small">{{ taskTypeLabel(row.taskType) }}</el-tag>
<el-tag :type="row.taskType === 'sysz' ? 'success' : 'info'" size="small">{{
taskTypeLabel(row.taskType)
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="itemName" label="检测项目" min-width="160" align="center" />
@@ -106,18 +146,10 @@
</el-table-column>
<el-table-column prop="conclusion" label="质检结论" min-width="100" align="center">
<template #default="{ row }">
<el-tag
v-if="row.conclusion === 'tg'"
type="success"
size="small"
>
<el-tag v-if="row.conclusion === 'tg'" type="success" size="small">
{{ row.conclusionLabel || qcTaskConclusionLabel(row.conclusion) }}
</el-tag>
<el-tag
v-else-if="row.conclusion === 'btg'"
type="danger"
size="small"
>
<el-tag v-else-if="row.conclusion === 'btg'" type="danger" size="small">
{{ row.conclusionLabel || qcTaskConclusionLabel(row.conclusion) }}
</el-tag>
<el-tag v-else type="info" size="small">—</el-tag>
@@ -125,24 +157,50 @@
</el-table-column>
<el-table-column prop="taskStatus" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.taskStatus === 'ywc' ? 'success' : row.taskStatus === 'jxz' ? 'warning' : 'info'" size="small">
<el-tag
:type="
row.taskStatus === 'ywc'
? 'success'
: row.taskStatus === 'jxz'
? 'warning'
: 'info'
"
size="small"
>
{{ taskStatusLabel(row.taskStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" align="center" show-overflow-tooltip>
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
<el-table-column label="操作" min-width="320" fixed="right" align="center">
<template #default="{ row }">
<template v-if="row.taskType === 'sysz' && row.taskStatus === 'dzx'">
<el-button class="btn-op-item primary" @click="handleStart(row)"><el-icon><VideoPlay /></el-icon>开始</el-button>
<el-button class="btn-op-item primary" @click="handleStart(row)"
><el-icon><VideoPlay /></el-icon>开始</el-button
>
</template>
<template v-if="canFillResult(row)">
<el-button class="btn-op-item primary" @click="openResult(row)"><el-icon><EditPen /></el-icon>填写结果</el-button>
<el-button class="btn-op-item primary" @click="openResult(row)"
><el-icon><EditPen /></el-icon>填写结果</el-button
>
</template>
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button v-if="row.taskStatus === 'dzx'" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button
v-if="row.taskStatus === 'dzx'"
class="btn-op-item"
@click="handleDelete(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -167,142 +225,165 @@
destroy-on-close
:close-on-click-modal="false"
>
<DialogFormBody
:loading="resultLoading"
>
<el-form label-position="top" class="biz-dialog-form">
<div class="form-section">
<div class="section-title">检测参数</div>
<div
v-if="!resultLoading && resultParams.length === 0"
style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px"
>
该项目暂无配置参数,请先在实验室/第三方项目中维护检测参数
</div>
<div class="form-grid-3">
<el-form-item
v-for="(param, idx) in resultParams"
:key="param.paramId || param.paramCode || idx"
:label="param.paramName + (param.paramUnit ? `(${param.paramUnit})` : '')"
<DialogFormBody :loading="resultLoading">
<el-form label-position="top" class="biz-dialog-form">
<div class="form-section">
<div class="section-title">检测参数</div>
<div
v-if="!resultLoading && resultParams.length === 0"
style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px"
>
<el-input v-if="param.paramType === 'text'" v-model="param.paramValue" placeholder="请输入" />
<el-input-number
v-else
:model-value="toResultNumber(param.paramValue)"
:precision="2"
style="width: 100%"
placeholder="请输入"
@update:model-value="(v) => setResultNumber(param, v)"
/>
</el-form-item>
</div>
</div>
<div v-if="resultTaskType !== 'dsfjc'" class="form-section material-usage-section">
<div class="section-title material-usage-head">
<div class="section-title-left"><h3>物料使用(选填)</h3></div>
<el-button class="btn-main" type="primary" size="small" @click="addMaterialRow">
<el-icon><Plus /></el-icon>添加物料
</el-button>
</div>
<el-table
v-if="materialUsages.length"
:data="materialUsages"
:row-key="(row) => row._rowKey"
stripe
class="list-table inner-table material-usage-table"
max-height="280"
>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column label="物料名称" min-width="160" align="center" header-align="center">
<template #default="{ row, $index }">
<el-select
v-model="row.materialId"
filterable
placeholder="搜索并选择物料"
class="inner-cell-input"
@change="(v) => onMaterialSelect($index, v)"
>
<el-option
v-for="opt in materialOptions"
:key="opt.id"
:label="opt.label"
:value="opt.id"
:disabled="isMaterialOptionDisabled(opt.id, $index)"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="规格型号" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ row.specModel || '—' }}</template>
</el-table-column>
<el-table-column label="用量" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-input-number
v-model="row.qty"
:min="0.0001"
:precision="4"
:step="1"
controls-position="right"
placeholder="用量"
class="inner-cell-input"
该项目暂无配置参数,请先在实验室/第三方项目中维护检测参数
</div>
<div class="form-grid-3">
<el-form-item
v-for="(param, idx) in resultParams"
:key="param.paramId || param.paramCode || idx"
:label="param.paramName + (param.paramUnit ? `(${param.paramUnit})` : '')"
>
<el-input
v-if="param.paramType === 'text'"
v-model="param.paramValue"
placeholder="请输入"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="72" align="center" header-align="center">
<template #default="{ row }">{{ row.unit || '—' }}</template>
</el-table-column>
<el-table-column label="备注" min-width="140" align="center" header-align="center">
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="选填" clearable class="inner-cell-input" />
</template>
</el-table-column>
<el-table-column label="操作" min-width="96" align="center" header-align="center" fixed="right">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="removeMaterialRow($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="暂无物料记录,可点击上方按钮添加"
:image-size="56"
/>
</div>
<div class="form-section result-conclusion-section">
<div class="section-title">检测结论</div>
<el-form-item label="结论" required>
<el-radio-group v-model="resultConclusion">
<el-radio value="tg">通过</el-radio>
<el-radio value="btg">不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="resultRemark" type="textarea" :rows="3" placeholder="选填" />
</el-form-item>
<div class="result-attachment-block">
<ProtocolFinanceConfirmFields
v-model:finance-remark="resultRemark"
v-model:attachments="resultAttachments"
:show-remark="false"
:attachment-label="resultAttachmentLabel"
:upload-folder="resultUploadFolder"
/>
<el-input-number
v-else
:model-value="toResultNumber(param.paramValue)"
:precision="2"
style="width: 100%"
placeholder="请输入"
@update:model-value="(v) => setResultNumber(param, v)"
/>
</el-form-item>
</div>
</div>
</div>
</el-form>
<div v-if="resultTaskType !== 'dsfjc'" class="form-section material-usage-section">
<div class="section-title material-usage-head">
<div class="section-title-left"><h3>物料使用(选填)</h3></div>
<el-button class="btn-main" type="primary" size="small" @click="addMaterialRow">
<el-icon><Plus /></el-icon>添加物料
</el-button>
</div>
<el-table
v-if="materialUsages.length"
:data="materialUsages"
:row-key="(row) => row._rowKey"
stripe
class="list-table inner-table material-usage-table"
max-height="280"
>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column
label="物料名称"
min-width="160"
align="center"
header-align="center"
>
<template #default="{ row, $index }">
<el-select
v-model="row.materialId"
filterable
placeholder="搜索并选择物料"
class="inner-cell-input"
@change="(v) => onMaterialSelect($index, v)"
>
<el-option
v-for="opt in materialOptions"
:key="opt.id"
:label="opt.label"
:value="opt.id"
:disabled="isMaterialOptionDisabled(opt.id, $index)"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="规格型号"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.specModel || '—' }}</template>
</el-table-column>
<el-table-column label="用量" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-input-number
v-model="row.qty"
:min="0.0001"
:precision="4"
:step="1"
controls-position="right"
placeholder="用量"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="72" align="center" header-align="center">
<template #default="{ row }">{{ row.unit || '—' }}</template>
</el-table-column>
<el-table-column label="备注" min-width="140" align="center" header-align="center">
<template #default="{ row }">
<el-input
v-model="row.remark"
placeholder="选填"
clearable
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="96"
align="center"
header-align="center"
fixed="right"
>
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="removeMaterialRow($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-empty v-else description="暂无物料记录,可点击上方按钮添加" :image-size="56" />
</div>
<div class="form-section result-conclusion-section">
<div class="section-title">检测结论</div>
<el-form-item label="结论" required>
<el-radio-group v-model="resultConclusion">
<el-radio value="tg">通过</el-radio>
<el-radio value="btg">不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="resultRemark" type="textarea" :rows="3" placeholder="选填" />
</el-form-item>
<div class="result-attachment-block">
<ProtocolFinanceConfirmFields
v-model:finance-remark="resultRemark"
v-model:attachments="resultAttachments"
:show-remark="false"
:attachment-label="resultAttachmentLabel"
:upload-folder="resultUploadFolder"
/>
</div>
</div>
</el-form>
</DialogFormBody>
<template #footer>
<el-button class="btn-ghost" @click="resultVisible = false"><el-icon><Close /></el-icon>取消</el-button>
<el-button class="btn-main" type="primary" :loading="resultLoading" @click="submitResult"><el-icon><Check /></el-icon>{{ resultSubmitLabel }}</el-button>
<el-button class="btn-ghost" @click="resultVisible = false"
><el-icon><Close /></el-icon>取消</el-button
>
<el-button class="btn-main" type="primary" :loading="resultLoading" @click="submitResult"
><el-icon><Check /></el-icon>{{ resultSubmitLabel }}</el-button
>
</template>
</el-dialog>
@@ -332,35 +413,40 @@
<template #material-usages>
<div v-if="viewMaterialUsages.length" class="view-subtable-wrap">
<el-table :data="viewMaterialUsages" stripe class="list-table view-subtable">
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column
prop="materialName"
label="物料名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column label="规格型号" min-width="120" align="center" header-align="center">
<template #default="{ row }">{{ row.specModel || '—' }}</template>
</el-table-column>
<el-table-column label="用量" min-width="100" align="center" header-align="center">
<template #default="{ row }">
{{ row.qty != null && row.qty !== '' ? row.qty : '—' }}
</template>
</el-table-column>
<el-table-column label="单位" min-width="72" align="center" header-align="center">
<template #default="{ row }">{{ row.unit || '—' }}</template>
</el-table-column>
<el-table-column label="备注" min-width="140" align="center" header-align="center">
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
<el-table-column
type="index"
label="序号"
min-width="56"
align="center"
header-align="center"
:index="(index) => index + 1"
/>
<el-table-column
prop="materialName"
label="物料名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
label="规格型号"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.specModel || '—' }}</template>
</el-table-column>
<el-table-column label="用量" min-width="100" align="center" header-align="center">
<template #default="{ row }">
{{ row.qty != null && row.qty !== '' ? row.qty : '—' }}
</template>
</el-table-column>
<el-table-column label="单位" min-width="72" align="center" header-align="center">
<template #default="{ row }">{{ row.unit || '—' }}</template>
</el-table-column>
<el-table-column label="备注" min-width="140" align="center" header-align="center">
<template #default="{ row }">{{ row.remark || '—' }}</template>
</el-table-column>
</el-table>
</div>
<p v-else class="view-empty-hint">暂无物料使用记录</p>
@@ -373,7 +459,6 @@
/>
</template>
</SystemViewDialog>
</section>
</div>
</template>
@@ -381,7 +466,17 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Close, Check, Search, RefreshLeft, VideoPlay, View, Tickets, EditPen, Plus } from '@element-plus/icons-vue'
import {
Close,
Check,
Search,
RefreshLeft,
VideoPlay,
View,
Tickets,
EditPen,
Plus
} from '@element-plus/icons-vue'
import { formatMoneyDisplay } from '@/utils/money-format'
import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
@@ -424,16 +519,20 @@
protocolStatusLabel,
protocolStatusTagType,
donationStatusLabel,
donationStatusTagType,
donationStatusTagType
} from '@/utils/status-tag-registry'
import type { TagType } from '@/utils/status-tag-registry'
/** 根据来源类型返回协议状态标签 */
function protocolStatusLabelBySource(status: string, source: string): string {
return source === 'research_donation' ? donationStatusLabel(status) : protocolStatusLabel(status)
return source === 'research_donation'
? donationStatusLabel(status)
: protocolStatusLabel(status)
}
function protocolStatusTagTypeBySource(status: string, source: string): TagType {
return source === 'research_donation' ? donationStatusTagType(status) : protocolStatusTagType(status)
return source === 'research_donation'
? donationStatusTagType(status)
: protocolStatusTagType(status)
}
/** 临时来源任务无关联样本,样本来源展示为 — */
@@ -536,9 +635,7 @@
const materialOptions = ref<QcMaterialOption[]>([])
const materialUsages = ref<MaterialUsageRow[]>([])
const resultDialogWidth = computed(() =>
resultTaskType.value === 'sysz' ? '970px' : '720px'
)
const resultDialogWidth = computed(() => (resultTaskType.value === 'sysz' ? '970px' : '720px'))
function canFillResult(row: QcTask) {
if (row.taskType === 'sysz') {
@@ -814,7 +911,10 @@
{ label: '样本编码', value: r.sampleCode || d.sampleCode || '—' },
{
label: '样本来源',
value: r.sourceTypeLabel || d.receptionSourceLabel || receptionSourceLabel(d.receptionSource || ''),
value:
r.sourceTypeLabel ||
d.receptionSourceLabel ||
receptionSourceLabel(d.receptionSource || ''),
tagType: 'primary' as const
},
{ label: '样本类型', value: r.sampleTypeName || d.sampleTypeName || '—' }
@@ -838,8 +938,7 @@
{
label: '结论',
value:
r.acceptConclusionLabel ||
acceptConclusionLabel(String(r.acceptConclusion || ''))
r.acceptConclusionLabel || acceptConclusionLabel(String(r.acceptConclusion || ''))
},
{ label: '检查项', value: formatCheckResults(r.checkResults), span: true },
{ label: '备注', value: r.remark || '—', span: true }
@@ -874,19 +973,40 @@
title: '任务信息',
fields: [
{ label: '任务号', value: d.taskNo },
{ label: '业务类型', value: d.qcBizTypeLabel || qcBizTypeLabel(d.qcBizType || ''), tagType: qcBizTypeTagType(d.qcBizType || '') },
{
label: '业务类型',
value: d.qcBizTypeLabel || qcBizTypeLabel(d.qcBizType || ''),
tagType: qcBizTypeTagType(d.qcBizType || '')
},
{ label: '工单号', value: d.workOrderNo },
{
label: '工单状态',
value: d.workOrderStatus ? workOrderStatusLabel(d.workOrderStatus) : '—',
tagType: d.workOrderStatus === 'ywc' ? 'success' : d.workOrderStatus === 'ypz' ? 'warning' : 'info'
tagType:
d.workOrderStatus === 'ywc'
? 'success'
: d.workOrderStatus === 'ypz'
? 'warning'
: 'info'
},
{
label: '检测类型',
value: taskTypeLabel(d.taskType),
tagType: d.taskType === 'sysz' ? 'success' : 'info'
},
{ label: '检测类型', value: taskTypeLabel(d.taskType), tagType: d.taskType === 'sysz' ? 'success' : 'info' },
{ label: '检测项目', value: d.itemName },
{ label: '项目编码', value: d.itemCode || '—' },
{ label: '第三方公司', value: d.companyName || '—' },
{ label: '检测费用', value: d.feeAmount != null ? '¥' + formatMoneyDisplay(d.feeAmount) : '—' },
{ label: '任务状态', value: taskStatusLabel(d.taskStatus), tagType: d.taskStatus === 'ywc' ? 'success' : d.taskStatus === 'jxz' ? 'warning' : 'info' },
{
label: '检测费用',
value: d.feeAmount != null ? '¥' + formatMoneyDisplay(d.feeAmount) : '—'
},
{
label: '任务状态',
value: taskStatusLabel(d.taskStatus),
tagType:
d.taskStatus === 'ywc' ? 'success' : d.taskStatus === 'jxz' ? 'warning' : 'info'
},
{
label: '检测结论',
value: d.conclusionLabel || qcTaskConclusionLabel(d.conclusion || ''),
@@ -1027,7 +1147,10 @@
ElMessage.warning('请选择检测结论')
return
}
if (resultTaskType.value === 'dsfjc' && !financeAttachmentsToUrlString(resultAttachments.value)) {
if (
resultTaskType.value === 'dsfjc' &&
!financeAttachmentsToUrlString(resultAttachments.value)
) {
ElMessage.warning('请上传第三方检测材料')
return
}
@@ -1065,7 +1188,8 @@
.filter((p) => hasResultValue(p.paramValue))
.map((p) => ({
...p,
paramValue: p.paramValue === undefined || p.paramValue === null ? '' : String(p.paramValue)
paramValue:
p.paramValue === undefined || p.paramValue === null ? '' : String(p.paramValue)
}))
}
data.thirdPartyAttachments = financeAttachmentsToUrlString(resultAttachments.value)
@@ -0,0 +1,441 @@
<!-- 出库质检工单 — 查看详情弹窗 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<SystemViewDialog
v-model:visible="dialogVisible"
title="出库质检工单详情"
width="1200px"
:grid-columns="4"
compact
:loading="loading"
:icon="Document"
:meta="detail?.workOrder?.workOrderNo || ''"
:sections="sections"
>
<template #diagnosis-attachments>
<ProtocolAttachmentReadonly
:files="diagnosisAttachmentFiles"
@preview="handlePreview"
@download="handleDownload"
/>
</template>
<template v-for="task in tasksWithAttachments" #[taskAttachmentSlotName(task.id)]>
<div :key="task.id" class="outbound-qc-task-attachments">
<ProtocolAttachmentReadonly
:files="task.attachmentFiles || []"
@preview="handleTaskPreview"
@download="handleTaskDownload"
/>
</div>
</template>
</SystemViewDialog>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { Document } from '@element-plus/icons-vue'
import {
fetchOutboundQcDetail,
type OutboundQcDetail,
type QcWorkOrderDetail
} from '@/api/qc-work-order'
import { fetchQcTaskResults, type QcTaskResultItem } from '@/api/qc-task-result'
import {
workOrderStatusLabel,
taskStatusLabel,
qcTaskConclusionLabel
} from '@/utils/qc-labels'
import {
scheduleStatusLabel,
scheduleStatusTagType,
infusionStatusLabel,
infusionStatusTagType,
qcStatusLabel,
qcStatusTagType,
applicationStatusLabel,
applicationStatusTagType,
bizTypeLabel,
bizTypeTagType,
productStatusLabel,
productStatusTagType
} from '@/utils/status-tag-registry'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { formatMoneyDisplay } from '@/utils/money-format'
defineOptions({ name: 'OutboundQcViewDialog' })
const props = defineProps<{
visible: boolean
workOrderId: string
}>()
const emit = defineEmits<{
(e: 'update:visible', v: boolean): void
}>()
const dialogVisible = computed({
get: () => props.visible,
set: (v) => emit('update:visible', v)
})
const loading = ref(false)
const detail = ref<OutboundQcDetail | null>(null)
type TaskView = {
id: string
taskNo: string
taskType: string
itemCode?: string
itemName?: string
taskStatus: string
conclusion?: string
conclusionLabel?: string
remark?: string
completedTime?: string
attachmentFiles?: Array<{ id?: string; fileUrl?: string; fileName?: string }>
results?: QcTaskResultItem[]
companyName?: string
feeAmount?: number
}
const tasksWithResults = ref<TaskView[]>([])
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
watch(
() => props.visible,
async (v) => {
if (!v || !props.workOrderId) return
loading.value = true
tasksWithResults.value = []
try {
const d = await fetchOutboundQcDetail(props.workOrderId)
detail.value = d
const rawTasks = (d.workOrder?.tasks || d.tasks || []) as TaskView[]
const enriched = await Promise.all(
rawTasks.map(async (task) => {
if (task.taskType !== 'sysz' || task.taskStatus !== 'ywc') {
return { ...task, results: [] as QcTaskResultItem[] }
}
try {
const results = await fetchQcTaskResults(task.id)
return { ...task, results: results || [] }
} catch {
return { ...task, results: [] as QcTaskResultItem[] }
}
})
)
tasksWithResults.value = enriched
} finally {
loading.value = false
}
}
)
// ── URL helpers ──────────────────────────────────────────────────────────────
function fullUrl(url: string) {
const u = String(url || '').trim()
if (!u) return ''
if (/^https?:\/\//i.test(u)) return u
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
}
// ── Diagnosis attachments ────────────────────────────────────────────────────
const diagnosisAttachmentFiles = computed(() => {
const urls = detail.value?.patient?.diagnosisAttachments || ''
if (!urls.trim()) return []
return urls
.split(',')
.map((u, i) => {
const url = u.trim()
if (!url) return null
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
return { id: `url_${i}`, fileUrl: fullUrl(url), fileName: name }
})
.filter(Boolean) as Array<Record<string, unknown>>
})
function handlePreview(file: Record<string, unknown>) {
const url = fullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: fullUrl })
}
// ── Task attachment helpers ─────────────────────────────────────────────────
const tasksWithAttachments = computed(() =>
tasksWithResults.value.filter((t) => (t.attachmentFiles?.length || 0) > 0)
)
function taskAttachmentSlotName(taskId: string) {
return `task-att-${taskId}`
}
function handleTaskPreview(file: Record<string, unknown>) {
const url = fullUrl(String(file.fileUrl || ''))
if (url) window.open(url, '_blank')
}
function handleTaskDownload(file: Record<string, unknown>) {
downloadAttachmentFile(file, { fullFileUrl: fullUrl })
}
// ── Tag helpers ─────────────────────────────────────────────────────────────
function conclusionTagType(conclusion?: string) {
if (conclusion === 'tg') return 'success' as const
if (conclusion === 'btg') return 'danger' as const
return 'info' as const
}
function taskStatusTagType(status?: string) {
if (status === 'ywc') return 'success' as const
if (status === 'jxz') return 'warning' as const
return 'info' as const
}
function workOrderStatusTagType(status?: string) {
if (status === 'ywc') return 'success' as const
if (status === 'ypz') return 'warning' as const
if (status === 'yzf') return 'danger' as const
return 'info' as const
}
function formatDetectMethods(wo?: Partial<QcWorkOrderDetail>) {
const parts: string[] = []
if (wo?.hasLab) parts.push('实验室')
if (wo?.hasThirdParty) parts.push('第三方')
return parts.length ? parts.join(' \\ ') : '—'
}
// ── Build task type sections (same pattern as index.vue) ────────────────────
function buildTaskTypeSections(
tasks: TaskView[],
taskType: 'sysz' | 'dsfjc',
sectionTitle: string
): ViewSection[] {
const filtered = tasks.filter((t) => t.taskType === taskType)
if (!filtered.length) return []
return filtered.map((task, index) => {
const title = filtered.length > 1 ? `${sectionTitle}(${index + 1})` : sectionTitle
const fields: ViewField[] = [
{ label: '检测项目', value: task.itemName || '—' },
{ label: '项目编码', value: task.itemCode || '—' },
{
label: '任务状态',
value: taskStatusLabel(task.taskStatus || ''),
tagType: taskStatusTagType(task.taskStatus)
},
{
label: '检测结论',
value: task.conclusionLabel || qcTaskConclusionLabel(task.conclusion || '') || '—',
tagType: conclusionTagType(task.conclusion)
},
{ label: '任务号', value: task.taskNo || '—' },
{ label: '完成时间', value: task.completedTime || '—' }
]
if (taskType === 'dsfjc') {
fields.splice(2, 0, { label: '第三方公司', value: task.companyName || '—' })
if (task.feeAmount != null) {
fields.push({ label: '费用(元)', value: String(task.feeAmount) })
}
}
if (task.results?.length) {
for (const item of task.results) {
fields.push({
label: item.paramName + (item.paramUnit ? `(${item.paramUnit})` : ''),
value: item.paramValue || '—'
})
}
} else if (taskType === 'sysz') {
fields.push({ label: '检测参数', value: '—' })
}
fields.push({ label: '备注', value: task.remark || '—', span: true })
const section: ViewSection = { title, fields }
if (task.attachmentFiles?.length) {
section.slot = taskAttachmentSlotName(task.id)
section.slotTitle = taskType === 'dsfjc' ? '检测材料' : '检测附件'
section.slotTitleStyle = 'field'
} else if (taskType === 'dsfjc') {
fields.push({ label: '检测材料', value: '—' })
} else {
fields.push({ label: '检测附件', value: '—' })
}
return section
})
}
// ── Sections computed ────────────────────────────────────────────────────────
const sections = computed<ViewSection[]>(() => {
if (!detail.value) return []
const d = detail.value
const wo: Partial<QcWorkOrderDetail> = d.workOrder || {}
const app = d.application
const patient = d.patient
const plan = d.infusionPlan
const schedule = d.infusionSchedule
const products = d.products || []
const result: ViewSection[] = []
// 1. 工单基本信息
result.push({
title: '工单基本信息',
fields: [
{ label: '工单号', value: wo.workOrderNo || '—' },
{ label: '工单类型', value: '出库', tagType: 'success' },
{
label: '业务类型',
value: wo.bizTypeLabel || bizTypeLabel(wo.bizType || ''),
tagType: bizTypeTagType(wo.bizType || '') as ViewField['tagType']
},
{
label: '状态',
value: workOrderStatusLabel(wo.workOrderStatus || ''),
tagType: workOrderStatusTagType(wo.workOrderStatus)
},
{ label: '检测方式', value: formatDetectMethods(wo) },
{ label: '配置时间', value: wo.configuredTime || '—' },
{ label: '配置人', value: wo.configuredBy || '—' },
{ label: '备注', value: wo.remark || '—', span: true },
{ label: '创建时间', value: wo.createTime || '—' }
]
})
// 2. 中试服务申请单信息
if (app) {
result.push({
title: '中试服务申请单信息',
fields: [
{ label: '申请单号', value: app.applicationNo || '—' },
{ label: '申请单名称', value: app.applicationName || '—' },
{
label: '申请单状态',
value: applicationStatusLabel(app.applicationStatus || ''),
tagType: applicationStatusTagType(app.applicationStatus || '') as ViewField['tagType']
},
{ label: '渠道名称', value: app.channelName || '—' }
]
})
}
// 3. 患者信息
if (patient) {
const patientFields: ViewField[] = [
{ label: '患者姓名', value: patient.patientName || '—' },
{ label: '性别', value: patient.sex || '—' },
{ label: '年龄', value: patient.age || '—' },
{ label: '联系电话', value: patient.phoneNo || '—' },
{ label: '身份证号', value: patient.idCardNo || '—' },
{ label: '紧急联系人', value: patient.emergencyContact || '—' },
{ label: '紧急联系电话', value: patient.emergencyPhone || '—' },
{ label: '临床诊断', value: patient.clinicalDiagnosis || '—', span: true }
]
result.push({
title: '患者信息',
fields: patientFields,
slotTitle: '诊断附件',
slotTitleStyle: 'field',
slot: 'diagnosis-attachments'
})
}
// 4. 输注方案信息
if (plan) {
result.push({
title: '输注方案信息',
fields: [
{ label: '细胞类型', value: plan.cellType || '—' },
{ label: '规格', value: plan.specText || '—' },
{ label: '总疗程数', value: plan.totalCourseCount != null ? String(plan.totalCourseCount) : '—' },
{ label: '疗程间隔', value: plan.courseInterval || '—' },
{ label: '输注途径', value: plan.infusionRoute || '—' },
{ label: '输注开始日期', value: plan.infusionStartDate || '—' }
]
})
}
// 5. 本次输注计划信息
if (schedule) {
const scheduleFields: ViewField[] = [
{ label: '输注时间', value: schedule.infusionTime || '—' },
{ label: '输注地点', value: schedule.infusionLocation || '—' },
{ label: '输注方式', value: schedule.infusionMethod || '—' },
{
label: '计划状态',
value: scheduleStatusLabel(schedule.scheduleStatus || ''),
tagType: scheduleStatusTagType(schedule.scheduleStatus || '') as ViewField['tagType']
},
{
label: '输注状态',
value: infusionStatusLabel(schedule.infusionStatus || ''),
tagType: infusionStatusTagType(schedule.infusionStatus || '') as ViewField['tagType']
},
{
label: '质检状态',
value: qcStatusLabel(schedule.qcStatus || ''),
tagType: qcStatusTagType(schedule.qcStatus || '') as ViewField['tagType']
}
]
if (schedule.cancelReason) {
scheduleFields.push({ label: '取消原因', value: schedule.cancelReason, span: true })
}
result.push({ title: '本次输注计划信息', fields: scheduleFields })
}
// 6. 产品明细
if (products.length) {
const productItems: string[] = products.map(
(p) =>
`${p.projectCode || '—'} / ${p.projectName || '—'} / ${p.cellTypeName || '—'} / ${p.specText || '—'} / 协议价:¥${p.agreementPrice != null ? formatMoneyDisplay(p.agreementPrice) : '—'} / 实际价:¥${p.actualPrice != null ? formatMoneyDisplay(p.actualPrice) : '—'} / 数量:${p.quantity ?? '—'} / ${productStatusLabel(p.productStatus || '')}`
)
result.push({
title: '产品明细',
fields: [{ label: '产品列表', value: '', span: true }],
slot: 'product-table'
})
// Override: we'll use items instead of a separate slot — use span field with items list
result[result.length - 1] = {
title: '产品明细',
fields: [
{
label: '产品列表',
span: true,
items: products.map(
(p) =>
`${p.projectCode || '—'} ${p.projectName || '—'} | ${p.cellTypeName || '—'} | ${p.specText || '—'} | 协议价 ¥${p.agreementPrice != null ? formatMoneyDisplay(p.agreementPrice) : '—'} | 实际价 ¥${p.actualPrice != null ? formatMoneyDisplay(p.actualPrice) : '—'} | 数量 ${p.quantity ?? '—'}`
)
}
]
}
}
// 7. 检测结果 — lab tasks
const labSections = buildTaskTypeSections(tasksWithResults.value, 'sysz', '实验室项目')
result.push(...labSections)
// 8. 检测结果 — third-party tasks
const thirdSections = buildTaskTypeSections(tasksWithResults.value, 'dsfjc', '第三方项目')
result.push(...thirdSections)
return result
})
</script>
<style scoped lang="scss">
.outbound-qc-task-attachments {
margin-top: 0;
}
</style>
@@ -19,7 +19,12 @@
<el-input v-model="searchForm.keyword" placeholder="编码/名称" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.status"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="启用" value="1" />
<el-option label="停用" value="0" />
</el-select>
@@ -76,16 +81,54 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="itemCode" label="检查项编码" min-width="140" align="center" header-align="center" />
<el-table-column prop="itemName" label="检查项名称" min-width="160" align="center" header-align="center" />
<el-table-column prop="orderNum" label="排序" min-width="80" align="center" header-align="center" />
<el-table-column prop="status" label="状态" min-width="90" align="center" header-align="center">
<el-table-column
prop="itemCode"
label="检查项编码"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="itemName"
label="检查项名称"
min-width="160"
align="center"
header-align="center"
/>
<el-table-column
prop="orderNum"
label="排序"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
prop="status"
label="状态"
min-width="90"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{ row.status === '1' ? '启用' : '停用' }}</el-tag>
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{
row.status === '1' ? '启用' : '停用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" header-align="center" />
<el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
<el-table-column
prop="createTime"
label="创建时间"
min-width="170"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
@@ -125,13 +168,22 @@
>
<div class="form-grid-2">
<el-form-item label="检查项编码" prop="itemCode">
<el-input v-model="formData.itemCode" placeholder="租户内唯一,如 packaging" :disabled="isEdit" />
<el-input
v-model="formData.itemCode"
placeholder="租户内唯一,如 packaging"
:disabled="isEdit"
/>
</el-form-item>
<el-form-item label="检查项名称" prop="itemName">
<el-input v-model="formData.itemName" placeholder="如:包装" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="formData.orderNum" :min="0" :max="9999" style="width: 100%" />
<el-input-number
v-model="formData.orderNum"
:min="0"
:max="9999"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
@@ -131,50 +131,144 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="protocolNo" label="协议号" min-width="130" align="center" header-align="center" />
<el-table-column prop="protocolStatus" label="协议状态" min-width="100" align="center" header-align="center">
<el-table-column
prop="protocolNo"
label="协议号"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="protocolStatus"
label="协议状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
:type="protocolStatusTagTypeBySource(String(row.protocolStatus || ''), row.sourceType || 'cell_storage')"
:type="
protocolStatusTagTypeBySource(
String(row.protocolStatus || ''),
row.sourceType || 'cell_storage'
)
"
size="small"
>{{ protocolStatusLabelBySource(String(row.protocolStatus || ''), row.sourceType || 'cell_storage') }}</el-tag>
>{{
protocolStatusLabelBySource(
String(row.protocolStatus || ''),
row.sourceType || 'cell_storage'
)
}}</el-tag
>
</template>
</el-table-column>
<el-table-column prop="sourceTypeLabel" label="来源类型" min-width="100" align="center" header-align="center">
<el-table-column
prop="sourceTypeLabel"
label="来源类型"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.sourceType === 'research_donation' ? 'warning' : 'success'" size="small">
<el-tag
:type="row.sourceType === 'research_donation' ? 'warning' : 'success'"
size="small"
>
{{ row.sourceTypeLabel || '细胞存储' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sampleCode" label="样本编码" min-width="120" align="center" header-align="center">
<el-table-column
prop="sampleCode"
label="样本编码"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.sampleCode || '-' }}</template>
</el-table-column>
<el-table-column prop="sampleTypeName" label="样本类型" min-width="110" align="center" header-align="center">
<el-table-column
prop="sampleTypeName"
label="样本类型"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.sampleTypeName || '-' }}</template>
</el-table-column>
<el-table-column prop="motherName" label="母亲姓名" min-width="100" align="center" header-align="center">
<el-table-column
prop="motherName"
label="母亲姓名"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.motherName || '-' }}</template>
</el-table-column>
<el-table-column prop="newbornName" label="新生儿姓名" min-width="110" align="center" header-align="center">
<el-table-column
prop="newbornName"
label="新生儿姓名"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.newbornName || '-' }}</template>
</el-table-column>
<el-table-column prop="etaTime" label="预计到达时间" min-width="170" align="center" header-align="center">
<el-table-column
prop="etaTime"
label="预计到达时间"
min-width="170"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.etaTime || '-' }}</template>
</el-table-column>
<el-table-column prop="transportType" label="运输方式" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ transportTypeLabel(String(row.transportType || '')) }}</template>
<el-table-column
prop="transportType"
label="运输方式"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{
transportTypeLabel(String(row.transportType || ''))
}}</template>
</el-table-column>
<el-table-column prop="waybillNo" label="快递单号" min-width="140" align="center" header-align="center">
<el-table-column
prop="waybillNo"
label="快递单号"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.waybillNo || '-' }}</template>
</el-table-column>
<el-table-column prop="expressCompanyName" label="快递公司" min-width="120" align="center" header-align="center">
<el-table-column
prop="expressCompanyName"
label="快递公司"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.expressCompanyName || '-' }}</template>
</el-table-column>
<el-table-column prop="carrierName" label="携带人" min-width="100" align="center" header-align="center">
<el-table-column
prop="carrierName"
label="携带人"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.carrierName || '-' }}</template>
</el-table-column>
<el-table-column prop="carrierPhone" label="携带人联系电话" min-width="130" align="center" header-align="center">
<el-table-column
prop="carrierPhone"
label="携带人联系电话"
min-width="130"
align="center"
header-align="center"
>
<template #default="{ row }">{{ row.carrierPhone || '-' }}</template>
</el-table-column>
<el-table-column
@@ -209,7 +303,13 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
<el-table-column
label="操作"
min-width="260"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button
v-if="activeTab === 'pending'"
@@ -251,13 +351,39 @@
<div v-if="currentRow" class="sample-reception-readonly">
<h4 class="detail-section-title">样本信息</h4>
<div class="readonly-grid form-grid-3">
<div class="readonly-item"><span class="label">协议号</span>{{ currentRow.protocolNo || '-' }}</div>
<div class="readonly-item"><span class="label">协议状态</span><el-tag :type="protocolStatusTagTypeBySource(String(currentRow.protocolStatus || ''), currentRow.sourceType || 'cell_storage')" size="small">{{ protocolStatusLabelBySource(String(currentRow.protocolStatus || ''), currentRow.sourceType || 'cell_storage') }}</el-tag></div>
<div class="readonly-item"><span class="label">样本编码</span>{{ currentRow.sampleCode || '-' }}</div>
<div class="readonly-item"><span class="label">样本类型</span>{{ currentRow.sampleTypeName || '-' }}</div>
<div class="readonly-item"><span class="label">预计到达</span>{{ currentRow.etaTime || '-' }}</div>
<div class="readonly-item"
><span class="label">协议号</span>{{ currentRow.protocolNo || '-' }}</div
>
<div class="readonly-item"
><span class="label">协议状态</span
><el-tag
:type="
protocolStatusTagTypeBySource(
String(currentRow.protocolStatus || ''),
currentRow.sourceType || 'cell_storage'
)
"
size="small"
>{{
protocolStatusLabelBySource(
String(currentRow.protocolStatus || ''),
currentRow.sourceType || 'cell_storage'
)
}}</el-tag
></div
>
<div class="readonly-item"
><span class="label">样本编码</span>{{ currentRow.sampleCode || '-' }}</div
>
<div class="readonly-item"
><span class="label">样本类型</span>{{ currentRow.sampleTypeName || '-' }}</div
>
<div class="readonly-item"
><span class="label">预计到达</span>{{ currentRow.etaTime || '-' }}</div
>
<div class="readonly-item readonly-col-span-2">
<span class="label">客户</span>{{ currentRow.motherName || '-' }} / {{ currentRow.newbornName || '-' }}
<span class="label">客户</span>{{ currentRow.motherName || '-' }} /
{{ currentRow.newbornName || '-' }}
</div>
</div>
</div>
@@ -265,16 +391,27 @@
<h4 class="detail-section-title">运输信息</h4>
<div class="readonly-grid form-grid-3">
<div class="readonly-item">
<span class="label">运输方式</span>{{ transportTypeLabel(String(currentRow?.transportType || '')) }}
<span class="label">运输方式</span
>{{ transportTypeLabel(String(currentRow?.transportType || '')) }}
</div>
<div class="readonly-item"><span class="label">预计到达</span>{{ currentRow?.etaTime || '-' }}</div>
<div class="readonly-item"
><span class="label">预计到达</span>{{ currentRow?.etaTime || '-' }}</div
>
<template v-if="currentRow?.transportType === 'kdys'">
<div class="readonly-item"><span class="label">快递公司</span>{{ currentRow?.expressCompanyName || '-' }}</div>
<div class="readonly-item"><span class="label">快递单号</span>{{ currentRow?.waybillNo || '-' }}</div>
<div class="readonly-item"
><span class="label">快递公司</span>{{ currentRow?.expressCompanyName || '-' }}</div
>
<div class="readonly-item"
><span class="label">快递单号</span>{{ currentRow?.waybillNo || '-' }}</div
>
</template>
<template v-else-if="currentRow?.transportType === 'ryxd'">
<div class="readonly-item"><span class="label">携带人</span>{{ currentRow?.carrierName || '-' }}</div>
<div class="readonly-item"><span class="label">携带人电话</span>{{ currentRow?.carrierPhone || '-' }}</div>
<div class="readonly-item"
><span class="label">携带人</span>{{ currentRow?.carrierName || '-' }}</div
>
<div class="readonly-item"
><span class="label">携带人电话</span>{{ currentRow?.carrierPhone || '-' }}</div
>
</template>
</div>
<div class="form-section-divider" />
@@ -352,7 +489,9 @@
{{ item.itemName }}
</el-checkbox>
</div>
<p v-else class="check-empty-hint">暂无启用的检查项,请先在「接收检查项配置」中维护。</p>
<p v-else class="check-empty-hint"
>暂无启用的检查项,请先在「接收检查项配置」中维护。</p
>
</el-form-item>
<ProtocolFinanceConfirmFields
v-model:finance-remark="receiveForm.remark"
@@ -366,7 +505,12 @@
<el-button class="btn-ghost" @click="receiveDialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="submitReceive">
<el-button
class="btn-main"
type="primary"
:loading="submitLoading"
@click="submitReceive"
>
<el-icon><Check /></el-icon>{{ receiveDialogMode === 'edit' ? '保存' : '提交' }}
</el-button>
</template>
@@ -383,38 +527,90 @@
<template v-if="viewRow">
<h4 class="detail-section-title">样本信息</h4>
<div class="readonly-grid form-grid-2">
<div class="readonly-item"><span class="label">协议号</span>{{ viewRow.protocolNo || '-' }}</div>
<div class="readonly-item"><span class="label">协议状态</span><el-tag :type="protocolStatusTagTypeBySource(String(viewRow.protocolStatus || ''), viewRow.sourceType || 'cell_storage')" size="small">{{ protocolStatusLabelBySource(String(viewRow.protocolStatus || ''), viewRow.sourceType || 'cell_storage') }}</el-tag></div>
<div class="readonly-item"><span class="label">样本编码</span>{{ viewRow.sampleCode || '-' }}</div>
<div class="readonly-item"><span class="label">样本类型</span>{{ viewRow.sampleTypeName || '-' }}</div>
<div class="readonly-item"><span class="label">预计到达</span>{{ viewRow.etaTime || '-' }}</div>
<div class="readonly-item"
><span class="label">协议号</span>{{ viewRow.protocolNo || '-' }}</div
>
<div class="readonly-item"
><span class="label">协议状态</span
><el-tag
:type="
protocolStatusTagTypeBySource(
String(viewRow.protocolStatus || ''),
viewRow.sourceType || 'cell_storage'
)
"
size="small"
>{{
protocolStatusLabelBySource(
String(viewRow.protocolStatus || ''),
viewRow.sourceType || 'cell_storage'
)
}}</el-tag
></div
>
<div class="readonly-item"
><span class="label">样本编码</span>{{ viewRow.sampleCode || '-' }}</div
>
<div class="readonly-item"
><span class="label">样本类型</span>{{ viewRow.sampleTypeName || '-' }}</div
>
<div class="readonly-item"
><span class="label">预计到达</span>{{ viewRow.etaTime || '-' }}</div
>
</div>
<div class="form-section-divider" />
<h4 class="detail-section-title">运输信息</h4>
<div class="readonly-grid form-grid-2">
<div class="readonly-item">
<span class="label">运输方式</span>{{ transportTypeLabel(String(viewRow.transportType || '')) }}
<span class="label">运输方式</span
>{{ transportTypeLabel(String(viewRow.transportType || '')) }}
</div>
<div class="readonly-item"><span class="label">预计到达</span>{{ viewRow.etaTime || '-' }}</div>
<div class="readonly-item"
><span class="label">预计到达</span>{{ viewRow.etaTime || '-' }}</div
>
<template v-if="viewRow.transportType === 'kdys'">
<div class="readonly-item"><span class="label">快递公司</span>{{ viewRow.expressCompanyName || '-' }}</div>
<div class="readonly-item"><span class="label">快递单号</span>{{ viewRow.waybillNo || '-' }}</div>
<div class="readonly-item"
><span class="label">快递公司</span>{{ viewRow.expressCompanyName || '-' }}</div
>
<div class="readonly-item"
><span class="label">快递单号</span>{{ viewRow.waybillNo || '-' }}</div
>
</template>
<template v-else-if="viewRow.transportType === 'ryxd'">
<div class="readonly-item"><span class="label">携带人</span>{{ viewRow.carrierName || '-' }}</div>
<div class="readonly-item"><span class="label">携带人电话</span>{{ viewRow.carrierPhone || '-' }}</div>
<div class="readonly-item"
><span class="label">携带人</span>{{ viewRow.carrierName || '-' }}</div
>
<div class="readonly-item"
><span class="label">携带人电话</span>{{ viewRow.carrierPhone || '-' }}</div
>
</template>
</div>
<div class="form-section-divider" />
<h4 class="detail-section-title">接收信息</h4>
<div class="readonly-grid form-grid-2">
<div class="readonly-item"><span class="label">接收时间</span>{{ viewRow.receptionTime || '-' }}</div>
<div class="readonly-item"><span class="label">经手人</span>{{ viewRow.handlerName || '-' }}</div>
<div class="readonly-item"><span class="label">接收温度(℃)</span>{{ viewRow.receiveTemperature ? viewRow.receiveTemperature + '℃' : '-' }}</div>
<div class="readonly-item"><span class="label">体积(ml)</span>{{ formatMetric(viewRow.volumeMl) }}</div>
<div class="readonly-item"><span class="label">净重(g)</span>{{ formatMetric(viewRow.netWeightG) }}</div>
<div class="readonly-item"
><span class="label">接收时间</span>{{ viewRow.receptionTime || '-' }}</div
>
<div class="readonly-item"
><span class="label">经手人</span>{{ viewRow.handlerName || '-' }}</div
>
<div class="readonly-item"
><span class="label">接收温度(℃)</span
>{{ viewRow.receiveTemperature ? viewRow.receiveTemperature + '℃' : '-' }}</div
>
<div class="readonly-item"
><span class="label">体积(ml)</span>{{ formatMetric(viewRow.volumeMl) }}</div
>
<div class="readonly-item"
><span class="label">净重(g)</span>{{ formatMetric(viewRow.netWeightG) }}</div
>
<div class="readonly-item">
<span class="label">结论</span><el-tag :type="viewRow.acceptConclusion === 'js' ? 'success' : 'danger'" size="small">{{ acceptConclusionLabel(String(viewRow.acceptConclusion || '')) }}</el-tag>
<span class="label">结论</span
><el-tag
:type="viewRow.acceptConclusion === 'js' ? 'success' : 'danger'"
size="small"
>{{ acceptConclusionLabel(String(viewRow.acceptConclusion || '')) }}</el-tag
>
</div>
<div class="readonly-item form-col-span-2 check-items-readonly">
<span class="label">检查项</span>
@@ -470,19 +666,53 @@
样本 {{ index + 1 }}:{{ entry.sampleCode || entry.protocolNo || '—' }}
</h4>
<div class="readonly-grid form-grid-2 batch-receive-card__meta">
<div class="readonly-item"><span class="label">协议号</span>{{ entry.protocolNo || '—' }}</div>
<div class="readonly-item"><span class="label">协议状态</span><el-tag :type="protocolStatusTagTypeBySource(String(entry.protocolStatus || ''), entry.sourceType || 'cell_storage')" size="small">{{ protocolStatusLabelBySource(String(entry.protocolStatus || ''), entry.sourceType || 'cell_storage') }}</el-tag></div>
<div class="readonly-item"><span class="label">样本类型</span>{{ entry.sampleTypeName || '—' }}</div>
<div class="readonly-item"
><span class="label">协议号</span>{{ entry.protocolNo || '—' }}</div
>
<div class="readonly-item"
><span class="label">协议状态</span
><el-tag
:type="
protocolStatusTagTypeBySource(
String(entry.protocolStatus || ''),
entry.sourceType || 'cell_storage'
)
"
size="small"
>{{
protocolStatusLabelBySource(
String(entry.protocolStatus || ''),
entry.sourceType || 'cell_storage'
)
}}</el-tag
></div
>
<div class="readonly-item"
><span class="label">样本类型</span>{{ entry.sampleTypeName || '—' }}</div
>
<div class="readonly-item form-col-span-2">
<span class="label">客户</span>{{ entry.motherName || '—' }} / {{ entry.newbornName || '—' }}
<span class="label">客户</span>{{ entry.motherName || '—' }} /
{{ entry.newbornName || '—' }}
</div>
<div class="readonly-item">
<span class="label">来源</span>
<el-tag :type="entry.sourceType === 'research_donation' ? 'warning' : 'success'" size="small">
<el-tag
:type="entry.sourceType === 'research_donation' ? 'warning' : 'success'"
size="small"
>
{{ entry.sourceType === 'research_donation' ? '科研捐赠' : '细胞存储' }}
</el-tag>
</div>
<div class="readonly-item"><span class="label">运输方式</span>{{ entry.transportType === 'kdys' ? '快递运输' : entry.transportType === 'ryxd' ? '人员携带' : '-' }}</div>
<div class="readonly-item"
><span class="label">运输方式</span
>{{
entry.transportType === 'kdys'
? '快递运输'
: entry.transportType === 'ryxd'
? '人员携带'
: '-'
}}</div
>
</div>
<el-form label-position="top" class="biz-dialog-form">
<div class="form-grid-3">
@@ -551,7 +781,9 @@
{{ item.itemName }}
</el-checkbox>
</div>
<p v-else class="check-empty-hint">暂无启用的检查项,请先在「接收检查项配置」中维护。</p>
<p v-else class="check-empty-hint"
>暂无启用的检查项,请先在「接收检查项配置」中维护。</p
>
</el-form-item>
<ProtocolFinanceConfirmFields
v-model:finance-remark="entry.remark"
@@ -567,7 +799,12 @@
<el-button class="btn-ghost" @click="batchReceiveDialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="submitBatchReceive">
<el-button
class="btn-main"
type="primary"
:loading="submitLoading"
@click="submitBatchReceive"
>
<el-icon><Check /></el-icon>提交全部
</el-button>
</template>
@@ -607,16 +844,20 @@
protocolStatusLabel,
protocolStatusTagType,
donationStatusLabel,
donationStatusTagType,
donationStatusTagType
} from '@/utils/status-tag-registry'
import type { TagType } from '@/utils/status-tag-registry'
/** 根据来源类型返回协议状态标签 */
function protocolStatusLabelBySource(status: string, source: string): string {
return source === 'research_donation' ? donationStatusLabel(status) : protocolStatusLabel(status)
return source === 'research_donation'
? donationStatusLabel(status)
: protocolStatusLabel(status)
}
function protocolStatusTagTypeBySource(status: string, source: string): TagType {
return source === 'research_donation' ? donationStatusTagType(status) : protocolStatusTagType(status)
return source === 'research_donation'
? donationStatusTagType(status)
: protocolStatusTagType(status)
}
defineOptions({ name: 'QualityInspectionSampleReception' })
@@ -787,9 +1028,7 @@
}
function applyCheckResultsFromDetail(results: SampleReceptionCheckResult[]) {
const map = new Map(
(results || []).map((r) => [String(r.checkItemId || ''), !!r.checked])
)
const map = new Map((results || []).map((r) => [String(r.checkItemId || ''), !!r.checked]))
for (const item of activeCheckItems.value) {
checkValues[item.id] = map.has(item.id) ? !!map.get(item.id) : false
}
@@ -801,13 +1040,11 @@
receiveForm.handlerName = String(detail.handlerName || '')
receiveForm.receiveTemperature = String(detail.receiveTemperature || '')
const vol = detail.volumeMl
receiveForm.volumeMl =
vol === null || vol === undefined || vol === '' ? undefined : Number(vol)
receiveForm.volumeMl = vol === null || vol === undefined || vol === '' ? undefined : Number(vol)
const net = detail.netWeightG
receiveForm.netWeightG =
net === null || net === undefined || net === '' ? undefined : Number(net)
receiveForm.acceptConclusion =
String(detail.acceptConclusion || 'js') === 'bj' ? 'bj' : 'js'
receiveForm.acceptConclusion = String(detail.acceptConclusion || 'js') === 'bj' ? 'bj' : 'js'
receiveForm.remark = String(detail.remark || '')
const checkList = detail.checkResults
applyCheckResultsFromDetail(
@@ -1011,12 +1248,10 @@
try {
await fetchSampleReceptionSubmit({
sourceType: entry.sourceType,
donationSampleId: entry.sourceType === 'research_donation'
? entry.protocolSampleId
: undefined,
protocolSampleId: entry.sourceType !== 'research_donation'
? entry.protocolSampleId
: undefined,
donationSampleId:
entry.sourceType === 'research_donation' ? entry.protocolSampleId : undefined,
protocolSampleId:
entry.sourceType !== 'research_donation' ? entry.protocolSampleId : undefined,
receptionTime: entry.receptionTime,
handlerUserId: entry.handlerUserId,
handlerName: entry.handlerName,
@@ -1229,12 +1464,14 @@
} else {
await fetchSampleReceptionSubmit({
sourceType: (currentRow.value.sourceType as string) || 'cell_storage',
donationSampleId: currentRow.value.sourceType === 'research_donation'
? String(currentRow.value.protocolSampleId)
: undefined,
protocolSampleId: currentRow.value.sourceType !== 'research_donation'
? String(currentRow.value.protocolSampleId)
: undefined,
donationSampleId:
currentRow.value.sourceType === 'research_donation'
? String(currentRow.value.protocolSampleId)
: undefined,
protocolSampleId:
currentRow.value.sourceType !== 'research_donation'
? String(currentRow.value.protocolSampleId)
: undefined,
...payload
})
receiveDialogVisible.value = false
@@ -119,9 +119,7 @@
return String(v)
}
function formatCheckResults(
items?: Array<{ itemName?: string; checked?: boolean }>
) {
function formatCheckResults(items?: Array<{ itemName?: string; checked?: boolean }>) {
if (!items?.length) return '—'
return items.map((x) => `${x.itemName || ''}${x.checked ? '(✓)' : '(×)'}`).join('、')
}
@@ -275,9 +273,14 @@
{ label: '净重(g)', value: formatMetric(r.netWeightG) },
{
label: '结论',
value: r.acceptConclusionLabel || acceptConclusionLabel(String(r.acceptConclusion || '')),
value:
r.acceptConclusionLabel || acceptConclusionLabel(String(r.acceptConclusion || '')),
tagType:
r.acceptConclusion === 'js' ? 'success' : r.acceptConclusion === 'jj' ? 'danger' : 'info'
r.acceptConclusion === 'js'
? 'success'
: r.acceptConclusion === 'jj'
? 'danger'
: 'info'
},
{ label: '检查项', value: formatCheckResults(r.checkResults), span: true },
{ label: '备注', value: r.remark || '—', span: true }
@@ -180,11 +180,7 @@
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="row.protocolStatus"
:type="protocolStatusTagForRow(row)"
size="small"
>
<el-tag v-if="row.protocolStatus" :type="protocolStatusTagForRow(row)" size="small">
{{ protocolStatusLabelForRow(row) }}
</el-tag>
<span v-else>—</span>
@@ -19,7 +19,12 @@
<el-input v-model="searchForm.keyword" placeholder="编码/名称" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100%">
<el-select
v-model="searchForm.status"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option label="启用" value="1" />
<el-option label="停用" value="0" />
</el-select>
@@ -73,15 +78,25 @@
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" />
<el-table-column prop="status" label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{ row.status === '1' ? '启用' : '停用' }}</el-tag>
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{
row.status === '1' ? '启用' : '停用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="400" fixed="right" align="center">
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button class="btn-op-item primary" @click="openItemDialog(row)"><el-icon><Setting /></el-icon>检测项目配置</el-button>
<el-button class="btn-op-item" @click="handleEdit(row)"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)"><el-icon><Delete /></el-icon>删除</el-button>
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="openItemDialog(row)"
><el-icon><Setting /></el-icon>检测项目配置</el-button
>
<el-button class="btn-op-item" @click="handleEdit(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDelete(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -107,10 +122,20 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form ref="companyFormRef" :model="companyForm" :rules="companyFormRules" label-position="top" class="biz-dialog-form">
<el-form
ref="companyFormRef"
:model="companyForm"
:rules="companyFormRules"
label-position="top"
class="biz-dialog-form"
>
<div class="form-grid-3">
<el-form-item label="公司编码" prop="companyCode">
<el-input v-model="companyForm.companyCode" :disabled="companyIsEdit" placeholder="租户内唯一" />
<el-input
v-model="companyForm.companyCode"
:disabled="companyIsEdit"
placeholder="租户内唯一"
/>
</el-form-item>
<el-form-item label="公司名称" prop="companyName">
<el-input v-model="companyForm.companyName" />
@@ -160,7 +185,12 @@
<el-button class="btn-ghost" @click="companyDialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="companySubmitLoading" @click="submitCompany">
<el-button
class="btn-main"
type="primary"
:loading="companySubmitLoading"
@click="submitCompany"
>
<el-icon><Check /></el-icon>确定
</el-button>
</template>
@@ -204,18 +234,32 @@
<el-table-column prop="itemCode" label="项目编码" min-width="120" align="center" />
<el-table-column prop="itemName" label="项目名称" min-width="160" align="center" />
<el-table-column prop="feeAmount" label="费用(元)" min-width="110" align="center">
<template #default="{ row }">{{ row.feeAmount != null ? '¥' + formatMoneyDisplay(row.feeAmount) : '—' }}</template>
<template #default="{ row }">{{
row.feeAmount != null ? '¥' + formatMoneyDisplay(row.feeAmount) : '—'
}}</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{ row.status === '1' ? '启用' : '停用' }}</el-tag>
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">{{
row.status === '1' ? '启用' : '停用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" align="center" show-overflow-tooltip />
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
show-overflow-tooltip
/>
<el-table-column label="操作" min-width="260" fixed="right" align="center">
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEditItem(row)"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button class="btn-op-item" @click="handleDeleteItem(row)"><el-icon><Delete /></el-icon>删除</el-button>
<el-button class="btn-op-item primary" @click="handleEditItem(row)"
><el-icon><Edit /></el-icon>编辑</el-button
>
<el-button class="btn-op-item" @click="handleDeleteItem(row)"
><el-icon><Delete /></el-icon>删除</el-button
>
</template>
</el-table-column>
</el-table>
@@ -226,7 +270,9 @@
:total="itemPagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(v) => ((itemPagination.page = 1), (itemPagination.pageSize = v), loadItems())"
@size-change="
(v) => ((itemPagination.page = 1), (itemPagination.pageSize = v), loadItems())
"
@current-change="(v) => ((itemPagination.page = v), loadItems())"
/>
</div>
@@ -296,14 +342,32 @@
header-align="center"
:index="(i) => i + 1"
/>
<el-table-column label="参数名称" min-width="140" align="center" header-align="center">
<el-table-column
label="参数名称"
min-width="140"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.paramName" placeholder="参数名称" class="inner-cell-input" />
<el-input
v-model="row.paramName"
placeholder="参数名称"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="参数编码" min-width="120" align="center" header-align="center">
<el-table-column
label="参数编码"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-input v-model="row.paramCode" placeholder="参数编码" class="inner-cell-input" />
<el-input
v-model="row.paramCode"
placeholder="参数编码"
class="inner-cell-input"
/>
</template>
</el-table-column>
<el-table-column label="单位" min-width="120" align="center" header-align="center">
@@ -343,15 +407,13 @@
</el-table-column>
<el-table-column label="操作" min-width="80" align="center" header-align="center">
<template #default="{ $index }">
<el-button link type="danger" size="small" @click="removeItemParam($index)">删除</el-button>
<el-button link type="danger" size="small" @click="removeItemParam($index)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<el-empty
v-else
description="暂无参数,点击上方按钮添加"
:image-size="48"
/>
<el-empty v-else description="暂无参数,点击上方按钮添加" :image-size="48" />
</div>
<div class="form-grid-2">
@@ -364,7 +426,12 @@
<el-button class="btn-ghost" @click="itemFormVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="itemSubmitLoading" @click="submitItem">
<el-button
class="btn-main"
type="primary"
:loading="itemSubmitLoading"
@click="submitItem"
>
<el-icon><Check /></el-icon>确定
</el-button>
</template>
@@ -390,7 +457,18 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, Search, RefreshLeft, View, Setting, Edit, Delete, OfficeBuilding } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
Search,
RefreshLeft,
View,
Setting,
Edit,
Delete,
OfficeBuilding
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import {
@@ -622,9 +700,13 @@
}
async function handleDelete(row: QcThirdPartyCompany) {
await ElMessageBox.confirm(`确定删除「${row.companyName}」?其下属检测项目将一并删除。`, '提示', {
type: 'warning'
})
await ElMessageBox.confirm(
`确定删除「${row.companyName}」?其下属检测项目将一并删除。`,
'提示',
{
type: 'warning'
}
)
await fetchDeleteQcThirdPartyCompany(row.id)
ElMessage.success('已删除')
loadData()
@@ -647,7 +729,11 @@
{ label: '联系人', value: row.contactName },
{ label: '联系电话', value: row.contactPhone },
{ label: '地址', value: row.address, span: true },
{ label: '状态', value: row.status === '1' ? '启用' : '停用', tagType: row.status === '1' ? 'success' : 'info' },
{
label: '状态',
value: row.status === '1' ? '启用' : '停用',
tagType: row.status === '1' ? 'success' : 'info'
},
{ label: '备注', value: row.remark, span: true }
]
},
@@ -57,7 +57,9 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span>
<span class="filter-hint"
>列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -152,9 +154,11 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="donationStatusTagType(String(row.donationStatus || ''))" size="small">{{
donationStatusLabel(String(row.donationStatus || ''))
}}</el-tag>
<el-tag
:type="donationStatusTagType(String(row.donationStatus || ''))"
size="small"
>{{ donationStatusLabel(String(row.donationStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column
@@ -9,10 +9,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<DialogFormBody
:loading="dialogFormLoading"
body-class="protocol-dialog-body min-h-[200px]"
>
<DialogFormBody :loading="dialogFormLoading" body-class="protocol-dialog-body min-h-[200px]">
<div class="detail-tabs">
<div
v-for="tab in formTabs"
@@ -670,9 +667,7 @@
<div
class="attachment-preview"
@click="
isImageFile(file) && file.fileUrl
? handlePreviewAttachment(file)
: undefined
isImageFile(file) && file.fileUrl ? handlePreviewAttachment(file) : undefined
"
>
<template v-if="isImageFile(file) && file.fileUrl">
@@ -711,7 +706,7 @@
</div>
</div>
</div>
<span class="upload-hint">支持图片和 PDF 格式,可稍后补充</span>
<span class="upload-hint">支持图片和 PDF 格式,可稍后补充</span>
</div>
<!-- 其他附件 -->
@@ -742,9 +737,7 @@
<div
class="attachment-preview"
@click="
isImageFile(file) && file.fileUrl
? handlePreviewAttachment(file)
: undefined
isImageFile(file) && file.fileUrl ? handlePreviewAttachment(file) : undefined
"
>
<template v-if="isImageFile(file) && file.fileUrl">
@@ -783,7 +776,7 @@
</div>
</div>
</div>
<span class="upload-hint">支持图片、PDF、Word 格式</span>
<span class="upload-hint">支持图片、PDF、Word 格式</span>
</div>
</div>
</div>
@@ -933,7 +926,16 @@
<script setup lang="ts">
import DialogFormBody from '@/components/business/dialog-form-body/index.vue'
import { ElMessage } from 'element-plus'
import { Plus, Close, Check, Delete, Document, VideoPlay, Loading, Download } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
Delete,
Document,
VideoPlay,
Loading,
Download
} from '@element-plus/icons-vue'
import {
fetchResearchDonationDetail,
fetchCreateResearchDonation,
@@ -1343,14 +1345,10 @@
function applyDonationBundleToForm(bundle: Record<string, any>) {
const donation = (bundle.donation ?? bundle.protocol) as Record<string, any> | null
if (donation) {
formData.collectorUserId = String(
donation.collectorUserId || formData.collectorUserId || ''
)
formData.collectorUserId = String(donation.collectorUserId || formData.collectorUserId || '')
formData.donationNo = String(donation.donationNo || formData.donationNo || '')
ensureDonationProtocolNoInOptions(formData.donationNo)
formData.donationStatus = String(
donation.donationStatus || formData.donationStatus || 'dsx'
)
formData.donationStatus = String(donation.donationStatus || formData.donationStatus || 'dsx')
formData.donationDate = String(donation.donationDate || formData.donationDate || '')
formData.remark = String(donation.remark ?? formData.remark ?? '')
}
@@ -1400,9 +1398,9 @@
if (consentAtt && consentAtt.length) {
formData.consentAttachments = consentAtt.map((a) => ({ ...a })) as any
}
const otherAtt = (donation?.attachmentFiles ?? (bundle as any).attachmentFiles) as
| Array<Record<string, any>>
| null
const otherAtt = (donation?.attachmentFiles ?? (bundle as any).attachmentFiles) as Array<
Record<string, any>
> | null
if (otherAtt && otherAtt.length) {
formData.otherAttachments = otherAtt.map((a) => ({ ...a })) as any
}
@@ -1493,9 +1491,7 @@
if (!sampleRow.sampleCodeId) continue
const sc = sampleCodeOptions.value.find((x) => x.id === sampleRow.sampleCodeId)
if (sc?.usedInProtocol) {
ElMessage.warning(
`样本编码「${sampleRow.sampleCode || sc.sampleCode}」已被占用,请更换`
)
ElMessage.warning(`样本编码「${sampleRow.sampleCode || sc.sampleCode}」已被占用,请更换`)
return
}
}
File diff suppressed because it is too large. Load diff
@@ -57,7 +57,9 @@
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span>
<span class="filter-hint"
>列表为概要信息,详情见弹窗;默认展示近 30 天内创建的协议单。</span
>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
@@ -120,9 +122,11 @@
header-align="center"
>
<template #default="{ row }">
<el-tag :type="donationStatusTagType(String(row.donationStatus || ''))" size="small">{{
donationStatusLabel(String(row.donationStatus || ''))
}}</el-tag>
<el-tag
:type="donationStatusTagType(String(row.donationStatus || ''))"
size="small"
>{{ donationStatusLabel(String(row.donationStatus || '')) }}</el-tag
>
</template>
</el-table-column>
<el-table-column
File diff suppressed because it is too large. Load diff
+20 -4
View File
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按组织名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -243,7 +247,15 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, OfficeBuilding, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
OfficeBuilding,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import {
fetchDeptTree,
@@ -472,7 +484,11 @@
{ label: '组织名称', value: viewRow.value?.deptName },
{ label: '上级组织', value: getParentDeptDisplay(viewRow.value?.parentId) },
{ label: '组织类别', value: getDeptCategoryLabel(viewRow.value?.deptCategoryCode || '') },
{ label: '状态', value: viewRow.value?.status === '1' ? '正常' : '停用', tagType: viewRow.value?.status === '1' ? 'success' : 'info' },
{
label: '状态',
value: viewRow.value?.status === '1' ? '正常' : '停用',
tagType: viewRow.value?.status === '1' ? 'success' : 'info'
},
{ label: '负责人', value: viewRow.value?.leader },
{ label: '联系电话', value: viewRow.value?.leaderPhone },
{ label: '备注', value: viewRow.value?.remark, span: true }
+31 -61
View File
@@ -15,18 +15,18 @@
<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-input v-model="searchForm.keyword" placeholder="字典名称/类型编码" clearable />
</el-form-item>
</div>
<div class="filter-actions-row">
<span class="filter-hint">按字典名称、类型编码模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -62,12 +62,7 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column
type="selection"
min-width="55"
align="center"
header-align="center"
/>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -115,18 +110,11 @@
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
<el-button
class="btn-op-item primary"
@click="handleDictData(row)"
>
<el-button class="btn-op-item primary" @click="handleDictData(row)">
字典数据配置
</el-button>
<el-button class="btn-op-item primary" @click="handleEdit(row)">
编辑
</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">
删除
</el-button>
<el-button class="btn-op-item primary" @click="handleEdit(row)"> 编辑 </el-button>
<el-button class="btn-op-item" @click="handleDelete(row)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
@@ -137,9 +125,7 @@
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="
(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())
"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
@@ -168,19 +154,10 @@
<el-input v-model="formData.dictName" placeholder="字典名称" />
</el-form-item>
<el-form-item label="字典类型编码" prop="dictType">
<el-input
v-model="formData.dictType"
placeholder="字典类型编码"
:disabled="isEdit"
/>
<el-input v-model="formData.dictType" placeholder="字典类型编码" :disabled="isEdit" />
</el-form-item>
<el-form-item label="备注" style="grid-column: 1 / -1">
<el-input
v-model="formData.remark"
type="textarea"
:rows="2"
placeholder="可选"
/>
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</div>
</el-form>
@@ -189,12 +166,7 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button
class="btn-main"
type="primary"
@click="handleSubmit"
:loading="submitLoading"
>
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
<el-icon><Check /></el-icon>
确定
</el-button>
@@ -292,8 +264,7 @@
align="center"
header-align="center"
:index="
(index) =>
(dictDataPagination.page - 1) * dictDataPagination.pageSize + index + 1
(index) => (dictDataPagination.page - 1) * dictDataPagination.pageSize + index + 1
"
/>
<el-table-column
@@ -326,15 +297,10 @@
header-align="center"
>
<template #default="{ row }">
<el-button
class="btn-op-item primary"
@click="handleEditDictData(row)"
>
<el-button class="btn-op-item primary" @click="handleEditDictData(row)">
编辑
</el-button>
<el-button class="btn-op-item" @click="handleDeleteDictData(row)">
删除
</el-button>
<el-button class="btn-op-item" @click="handleDeleteDictData(row)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
@@ -352,9 +318,7 @@
loadDictData()
)
"
@current-change="
(val) => ((dictDataPagination.page = val), loadDictData())
"
@current-change="(val) => ((dictDataPagination.page = val), loadDictData())"
/>
</div>
</div>
@@ -418,7 +382,15 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Collection, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
View,
Collection,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
@@ -704,11 +676,9 @@
}
async function handleDeleteDictData(row: Api.Dict.DictDataItem) {
await ElMessageBox.confirm(
`确定删除字典数据「${row.dictLabel}」吗?`,
'提示',
{ type: 'warning' }
)
await ElMessageBox.confirm(`确定删除字典数据「${row.dictLabel}」吗?`, '提示', {
type: 'warning'
})
await fetchDeleteDictData(row.id)
await loadDictData()
}
+40 -9
View File
@@ -22,8 +22,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按快递公司名称、编码模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -68,10 +72,34 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="expressCode" label="快递编码" min-width="120" align="center" header-align="center" />
<el-table-column prop="expressName" label="快递名称" min-width="140" align="center" header-align="center" />
<el-table-column prop="createTime" label="创建时间" min-width="180" align="center" header-align="center" />
<el-table-column label="操作" min-width="320" fixed="right" align="center" header-align="center">
<el-table-column
prop="expressCode"
label="快递编码"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="expressName"
label="快递名称"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
prop="createTime"
label="创建时间"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
min-width="320"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
@@ -292,7 +320,9 @@
}
async function handleDelete(row: Api.ExpressCompany.ExpressCompanyItem) {
await ElMessageBox.confirm(`确定删除快递公司「${row.expressName}」吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除快递公司「${row.expressName}」吗?`, '提示', {
type: 'warning'
})
await fetchDeleteExpressCompany(row.id)
ElMessage.success('删除成功')
await loadData()
@@ -301,7 +331,9 @@
async function handleBatchDelete() {
const ids = selectedRows.value.map((r) => r.id)
if (!ids.length) return
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个快递公司吗?`, '提示', { type: 'warning' })
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个快递公司吗?`, '提示', {
type: 'warning'
})
await withFullscreenLoading(async () => {
for (const id of ids) {
await fetchDeleteExpressCompany(id)
@@ -331,6 +363,5 @@
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
+202 -136
View File
@@ -18,7 +18,12 @@
<el-input v-model="filterForm.keyword" placeholder="搜索原因/类型" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filterForm.status" placeholder="全部" clearable style="width: 120px">
<el-select
v-model="filterForm.status"
placeholder="全部"
clearable
style="width: 120px"
>
<el-option label="草稿" value="draft" />
<el-option label="审批中" value="pending" />
<el-option label="已通过" value="approved" />
@@ -29,8 +34,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按原因、类型模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -55,23 +64,73 @@
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column prop="leaveTypeLabel" label="请假类型" min-width="100" align="center" header-align="center" />
<el-table-column prop="startDate" label="开始日期" min-width="120" align="center" header-align="center" />
<el-table-column prop="endDate" label="结束日期" min-width="120" align="center" header-align="center" />
<el-table-column prop="days" label="天数" min-width="80" align="center" header-align="center" />
<el-table-column
prop="leaveTypeLabel"
label="请假类型"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="startDate"
label="开始日期"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="endDate"
label="结束日期"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column
prop="days"
label="天数"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column prop="reason" label="请假原因" min-width="180" show-overflow-tooltip />
<el-table-column prop="status" label="状态" min-width="100" align="center" header-align="center">
<el-table-column
prop="status"
label="状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
<el-tag :type="statusTagType(row.status)" size="small">{{
statusLabel(row.status)
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="180" align="center" header-align="center" />
<el-table-column label="操作" fixed="right" min-width="260" align="center" header-align="center">
<el-table-column
prop="createTime"
label="创建时间"
min-width="180"
align="center"
header-align="center"
/>
<el-table-column
label="操作"
fixed="right"
min-width="260"
align="center"
header-align="center"
>
<template #default="{ row }">
<template v-if="row.status === 'draft' || row.status === 'rejected'">
<el-button type="primary" link size="small" @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link size="small" @click="handleSubmit(row.id)">提交审批</el-button>
<el-button type="danger" link size="small" @click="handleDelete(row.id)">删除</el-button>
<el-button type="primary" link size="small" @click="handleEdit(row)"
>编辑</el-button
>
<el-button type="success" link size="small" @click="handleSubmit(row.id)"
>提交审批</el-button
>
<el-button type="danger" link size="small" @click="handleDelete(row.id)"
>删除</el-button
>
</template>
<template v-else-if="row.status === 'pending'">
<el-tag type="warning" size="small">审批中</el-tag>
@@ -154,7 +213,12 @@
/>
</el-form-item>
<el-form-item label="请假原因" class="form-col-span-2">
<el-input v-model="formData.reason" type="textarea" :rows="3" placeholder="请输入请假原因" />
<el-input
v-model="formData.reason"
type="textarea"
:rows="3"
placeholder="请输入请假原因"
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="formData.remark" placeholder="选填" />
@@ -176,144 +240,146 @@
</template>
<script setup lang="ts">
import { Close, Check, RefreshLeft, Search } from '@element-plus/icons-vue'
import { leaveStatusLabel, leaveStatusTagType } from '@/utils/status-tag-registry'
import {
fetchLeaveList,
fetchCreateLeave,
fetchUpdateLeave,
fetchDeleteLeave,
fetchSubmitLeave
} from '@/api/leave'
import { Close, Check, RefreshLeft, Search } from '@element-plus/icons-vue'
import { leaveStatusLabel, leaveStatusTagType } from '@/utils/status-tag-registry'
import {
fetchLeaveList,
fetchCreateLeave,
fetchUpdateLeave,
fetchDeleteLeave,
fetchSubmitLeave
} from '@/api/leave'
defineOptions({ name: 'LeaveRequest' })
defineOptions({ name: 'LeaveRequest' })
const loading = ref(false)
const tableData = ref<Api.Leave.LeaveItem[]>([])
const selectedRows = ref<Api.Leave.LeaveItem[]>([])
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const loading = ref(false)
const tableData = ref<Api.Leave.LeaveItem[]>([])
const selectedRows = ref<Api.Leave.LeaveItem[]>([])
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const filterForm = reactive({ keyword: '', status: '' })
const filterForm = reactive({ keyword: '', status: '' })
const dialogVisible = ref(false)
const isEdit = ref(false)
const editingId = ref('')
const submitting = ref(false)
const dialogVisible = ref(false)
const isEdit = ref(false)
const editingId = ref('')
const submitting = ref(false)
const formData = reactive({
leaveType: 'personal',
startDate: '',
endDate: '',
days: 1,
reason: '',
remark: ''
})
const formData = reactive({
leaveType: 'personal',
startDate: '',
endDate: '',
days: 1,
reason: '',
remark: ''
})
const statusLabel = leaveStatusLabel
const statusTagType = leaveStatusTagType
const statusLabel = leaveStatusLabel
const statusTagType = leaveStatusTagType
function resetForm() {
formData.leaveType = 'personal'
formData.startDate = ''
formData.endDate = ''
formData.days = 1
formData.reason = ''
formData.remark = ''
}
function handleSelectionChange(rows: Api.Leave.LeaveItem[]) {
selectedRows.value = rows
}
async function loadData() {
loading.value = true
try {
const res = await fetchLeaveList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: filterForm.keyword || undefined,
status: filterForm.status || undefined
})
tableData.value = res.list || []
pagination.total = res.total || 0
} catch {
// error shown by http util
} finally {
loading.value = false
function resetForm() {
formData.leaveType = 'personal'
formData.startDate = ''
formData.endDate = ''
formData.days = 1
formData.reason = ''
formData.remark = ''
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
filterForm.keyword = ''
filterForm.status = ''
pagination.page = 1
loadData()
}
function handleAdd() {
isEdit.value = false
editingId.value = ''
resetForm()
dialogVisible.value = true
}
function handleEdit(row: Api.Leave.LeaveItem) {
isEdit.value = true
editingId.value = row.id
formData.leaveType = row.leaveType
formData.startDate = row.startDate
formData.endDate = row.endDate
formData.days = row.days
formData.reason = row.reason
formData.remark = row.remark || ''
dialogVisible.value = true
}
async function handleSave() {
if (!formData.leaveType || !formData.startDate || !formData.endDate || !formData.days) {
ElMessage.warning('请填写必填项')
return
function handleSelectionChange(rows: Api.Leave.LeaveItem[]) {
selectedRows.value = rows
}
submitting.value = true
try {
if (isEdit.value) {
await fetchUpdateLeave(editingId.value, { ...formData })
} else {
await fetchCreateLeave({ ...formData })
async function loadData() {
loading.value = true
try {
const res = await fetchLeaveList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: filterForm.keyword || undefined,
status: filterForm.status || undefined
})
tableData.value = res.list || []
pagination.total = res.total || 0
} catch {
// error shown by http util
} finally {
loading.value = false
}
dialogVisible.value = false
loadData()
} catch {
// error shown by http util
} finally {
submitting.value = false
}
}
async function handleSubmit(id: string) {
try {
await ElMessageBox.confirm('确定提交审批?提交后将进入审批流程。', '提示', { type: 'warning' })
await fetchSubmitLeave(id)
function handleSearch() {
pagination.page = 1
loadData()
} catch {
// cancelled or error
}
}
async function handleDelete(id: string) {
try {
await ElMessageBox.confirm('确定删除此请假申请?', '提示', { type: 'warning' })
await fetchDeleteLeave(id)
function handleReset() {
filterForm.keyword = ''
filterForm.status = ''
pagination.page = 1
loadData()
} catch {
// cancelled or error
}
}
function handleAdd() {
isEdit.value = false
editingId.value = ''
resetForm()
dialogVisible.value = true
}
function handleEdit(row: Api.Leave.LeaveItem) {
isEdit.value = true
editingId.value = row.id
formData.leaveType = row.leaveType
formData.startDate = row.startDate
formData.endDate = row.endDate
formData.days = row.days
formData.reason = row.reason
formData.remark = row.remark || ''
dialogVisible.value = true
}
async function handleSave() {
if (!formData.leaveType || !formData.startDate || !formData.endDate || !formData.days) {
ElMessage.warning('请填写必填项')
return
}
submitting.value = true
try {
if (isEdit.value) {
await fetchUpdateLeave(editingId.value, { ...formData })
} else {
await fetchCreateLeave({ ...formData })
}
dialogVisible.value = false
loadData()
} catch {
// error shown by http util
} finally {
submitting.value = false
}
}
async function handleSubmit(id: string) {
try {
await ElMessageBox.confirm('确定提交审批?提交后将进入审批流程。', '提示', {
type: 'warning'
})
await fetchSubmitLeave(id)
loadData()
} catch {
// cancelled or error
}
}
async function handleDelete(id: string) {
try {
await ElMessageBox.confirm('确定删除此请假申请?', '提示', { type: 'warning' })
await fetchDeleteLeave(id)
loadData()
} catch {
// cancelled or error
}
}
// 输入检索条件时自动搜索(防抖 300ms)
watchDebounced(
filterForm,
@@ -324,5 +390,5 @@ async function handleDelete(id: string) {
{ debounce: 300, deep: true }
)
onMounted(() => loadData())
onMounted(() => loadData())
</script>
+42 -42
View File
@@ -21,8 +21,12 @@
<div class="filter-actions-row">
<span class="filter-hint">按菜单名称模糊查询。</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
<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>
@@ -56,12 +60,7 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column
type="selection"
min-width="55"
align="center"
header-align="center"
/>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
prop="menuName"
label="菜单名称"
@@ -77,10 +76,7 @@
header-align="center"
>
<template #default="{ row }">
<el-tag
:type="menuTypeTagMap[row.menuType]?.type || 'info'"
size="small"
>
<el-tag :type="menuTypeTagMap[row.menuType]?.type || 'info'" size="small">
{{ menuTypeTagMap[row.menuType]?.label || row.menuType }}
</el-tag>
</template>
@@ -144,13 +140,13 @@
</el-tag>
</template>
</el-table-column>
<el-table-column
label="操作"
min-width="400"
fixed="right"
align="center"
header-align="center"
>
<el-table-column
label="操作"
min-width="400"
fixed="right"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)">
<el-icon><View /></el-icon>查看
@@ -230,12 +226,7 @@
</el-select>
</el-form-item>
<el-form-item label="备注" style="grid-column: 1 / -1">
<el-input
v-model="formData.remark"
type="textarea"
:rows="2"
placeholder="可选"
/>
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</div>
</el-form>
@@ -244,12 +235,7 @@
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button
class="btn-main"
type="primary"
@click="handleSubmit"
:loading="submitLoading"
>
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
<el-icon><Check /></el-icon>
确定
</el-button>
@@ -270,14 +256,17 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Menu as MenuIcon, RefreshLeft, Search } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import {
fetchMenuTree,
fetchCreateMenu,
fetchUpdateMenu,
fetchDeleteMenu
} from '@/api/admin'
Plus,
Close,
Check,
View,
Menu as MenuIcon,
RefreshLeft,
Search
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { fetchMenuTree, fetchCreateMenu, fetchUpdateMenu, fetchDeleteMenu } from '@/api/admin'
defineOptions({ name: 'SystemMenu' })
@@ -343,13 +332,25 @@
title: '基本信息',
fields: [
{ label: '菜单名称', value: viewRow.value?.menuName },
{ label: '菜单类型', value: menuTypeTagMap[viewRow.value?.menuType || '']?.label || viewRow.value?.menuType, tagType: 'primary' },
{
label: '菜单类型',
value: menuTypeTagMap[viewRow.value?.menuType || '']?.label || viewRow.value?.menuType,
tagType: 'primary'
},
{ label: '路由路径', value: viewRow.value?.path },
{ label: '组件路径', value: viewRow.value?.component },
{ label: '权限标识', value: viewRow.value?.perms },
{ label: '是否外链', value: viewRow.value?.isFrame === 1 ? '否' : '是' },
{ label: '显示状态', value: viewRow.value?.visible === '1' ? '显示' : '隐藏', tagType: viewRow.value?.visible === '1' ? 'success' : 'info' },
{ label: '状态', value: viewRow.value?.status === '1' ? '正常' : '停用', tagType: viewRow.value?.status === '1' ? 'success' : 'info' },
{
label: '显示状态',
value: viewRow.value?.visible === '1' ? '显示' : '隐藏',
tagType: viewRow.value?.visible === '1' ? 'success' : 'info'
},
{
label: '状态',
value: viewRow.value?.status === '1' ? '正常' : '停用',
tagType: viewRow.value?.status === '1' ? 'success' : 'info'
},
{ label: '备注', value: viewRow.value?.remark, span: true }
]
}
@@ -544,6 +545,5 @@
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
File diff suppressed because it is too large. Load diff
Loaded 100 of 1503 files, more files were not shown because too many files have changed in this diff. Show more