This commit is contained in:
zlwu committed 2026-05-20 09:28:36 +08:00
1 parent e6072457fd
commit be4fab78e1
578 files changed
+174415 -214

No files matched your search

@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">细胞样本类型列表</h2>
<h2>细胞样本类型列表</h2>
</div>
<div class="filter-block">
@@ -34,12 +34,11 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增类型
</el-button>
<el-button
v-if="hasAuth('delete')"
class="btn-ghost"
type="danger"
:disabled="selectedRows.length === 0"
@@ -117,8 +116,8 @@
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button v-if="hasAuth('edit')" class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="hasAuth('delete')" 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>
@@ -219,8 +218,6 @@
fetchDeleteCellSampleType
} from '@/api/admin'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { useAuth } from '@/hooks/core/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'BizCellSampleType' })
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">采集协议号列表</h2>
<h2>采集协议号列表</h2>
</div>
<div class="filter-block">
@@ -34,11 +34,11 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增协议号
</el-button>
<el-button v-if="hasAuth('add')" class="btn-ghost" @click="openBatchGenerate">
<el-button class="btn-ghost" @click="openBatchGenerate">
批量生成
</el-button>
<el-button
@@ -56,7 +56,6 @@
批量收回
</el-button>
<el-button
v-if="hasAuth('delete')"
class="btn-ghost"
type="danger"
:disabled="selectedRows.length === 0"
@@ -138,8 +137,8 @@
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button v-if="hasAuth('edit')" class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="hasAuth('delete')" 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>
@@ -377,8 +376,6 @@
fetchBatchIssueProtocolNos,
fetchBatchRevokeProtocolNos
} from '@/api/admin'
import { useAuth } from '@/hooks/core/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'BizCollectionProtocolNo' })
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">采集地区列表</h2>
<h2>采集地区列表</h2>
</div>
<div class="filter-block">
@@ -34,12 +34,11 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增地区
</el-button>
<el-button
v-if="hasAuth('delete')"
class="btn-ghost"
type="danger"
:disabled="selectedRows.length === 0"
@@ -114,8 +113,8 @@
<el-button class="btn-op-item primary" @click="openHospitalDialog(row)"
>地区医院配置</el-button
>
<el-button v-if="hasAuth('edit')" class="btn-op-item" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="hasAuth('delete')" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
<el-button class="btn-op-item" @click="handleEdit(row)">编辑</el-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
@@ -346,8 +345,6 @@
fetchUpdateHospital,
fetchDeleteHospital
} from '@/api/admin'
import { useAuth } from '@/hooks/core/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'BizCollectionRegion' })
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">客户列表</h2>
<h2>客户列表</h2>
</div>
<div class="filter-block">
@@ -38,12 +38,11 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增客户
</el-button>
<el-button
v-if="hasAuth('delete')"
class="btn-ghost"
type="danger"
:disabled="selectedRows.length === 0"
@@ -126,8 +125,8 @@
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button v-if="hasAuth('edit')" class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="hasAuth('delete')" 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>
@@ -236,9 +235,7 @@
fetchDeleteCustomer
} from '@/api/admin'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { useAuth } from '@/hooks/core/useAuth'
import { sexLabel } from '@/utils/status-tag-registry'
const { hasAuth } = useAuth()
defineOptions({ name: 'BizCustomer' })
@@ -0,0 +1,735 @@
<!-- 保险公司 — 项目价格配置 -->
<!-- eslint-disable vue/no-v-model-argument -->
<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.tag" placeholder="全部" clearable style="width: 100%">
<el-option label="正常" value="1" />
<el-option label="停用" value="0" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions-row">
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增保险公司
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
width="68"
align="center"
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="状态" 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 label="操作" 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>
</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) => ((pagination.page = 1), (pagination.pageSize = v), loadData())"
@current-change="(v) => ((pagination.page = v), loadData())"
/>
</div>
</div>
<!-- 新增/编辑保险公司弹窗 -->
<el-dialog
v-model="companyDialogVisible"
:title="companyDialogTitle"
width="720px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<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="请输入保险公司名称" />
</el-form-item>
<el-form-item label="公司地址">
<el-input v-model="companyForm.companyAddress" placeholder="请输入公司地址" />
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="companyForm.contactName" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="companyForm.contactPhone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="状态" prop="tag">
<el-radio-group v-model="companyForm.tag">
<el-radio :value="1">正常</el-radio>
<el-radio :value="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="companyForm.remark" type="textarea" :rows="2" />
</el-form-item>
</div>
<div class="form-section">
<div class="section-title">开票信息</div>
<div class="form-grid-2">
<el-form-item label="发票抬头">
<el-input v-model="companyForm.invoiceTitle" placeholder="与营业执照一致" />
</el-form-item>
<el-form-item label="纳税人识别号">
<el-input v-model="companyForm.taxNo" placeholder="统一社会信用代码" />
</el-form-item>
<el-form-item label="开户银行">
<el-input v-model="companyForm.bankName" placeholder="收款银行全称" />
</el-form-item>
<el-form-item label="银行账号">
<el-input v-model="companyForm.bankAccount" placeholder="收款账号" />
</el-form-item>
</div>
</div>
</el-form>
<template #footer>
<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-icon><Check /></el-icon>确定
</el-button>
</template>
</el-dialog>
<!-- 项目价格配置弹窗 -->
<el-dialog
v-model="itemDialogVisible"
:title="itemDialogTitle"
width="960px"
class="dialog-form-layout insurance-item-config-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
@closed="onItemDialogClosed"
>
<div class="item-dialog-head">
<span class="muted">当前公司:{{ currentCompanyName }}</span>
</div>
<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" width="55" align="center" header-align="center" />
<el-table-column type="index" label="序号" 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="项目费用(元)" width="120" align="center" header-align="center">
<template #default="{ row }">{{ row.projectFee != null ? `¥${row.projectFee}` : '—' }}</template>
</el-table-column>
<el-table-column prop="status" label="状态" 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="操作" 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-dialog
v-model="itemFormVisible"
:title="itemFormTitle"
width="640px"
class="dialog-form-layout"
align-center
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
<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" />
</el-form-item>
<el-form-item label="项目费用(元)" prop="projectFee">
<el-input-number v-model="itemForm.projectFee" :min="0" :precision="2" style="width: 100%" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="itemForm.status">
<el-radio value="1">正常</el-radio>
<el-radio value="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2">
<el-input v-model="itemForm.remark" type="textarea" :rows="2" />
</el-form-item>
</div>
</el-form>
<template #footer>
<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-icon><Check /></el-icon>确定
</el-button>
</template>
</el-dialog>
<template #footer>
<el-button class="btn-ghost" @click="itemDialogVisible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
<!-- 查看弹窗 -->
<SystemViewDialog
v-model:visible="viewVisible"
title="查看保险公司"
:icon="OfficeBuilding"
:meta="viewCompanyName"
:sections="viewSections"
/>
</section>
</div>
</template>
<script setup lang="ts">
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 {
fetchInsuranceCompanyList,
fetchCreateInsuranceCompany,
fetchUpdateInsuranceCompany,
fetchDeleteInsuranceCompany,
fetchInsuranceCompanyInvoices,
fetchCreateInsuranceCompanyInvoice,
fetchUpdateInsuranceCompanyInvoice,
fetchDeleteInsuranceCompanyInvoice,
fetchInsuranceProjectPrices,
fetchCreateInsuranceProjectPrice,
fetchUpdateInsuranceProjectPrice,
fetchDeleteInsuranceProjectPrice
} from '@/api/admin'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
defineOptions({ name: 'BasicInfoInsuranceCompany' })
// ---------------------------------------------------------------------------
// 搜索 & 主表
// ---------------------------------------------------------------------------
const searchForm = reactive({ keyword: '', tag: '' })
const tableData = ref<any[]>([])
const selectedRows = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
// ---------------------------------------------------------------------------
// 保险公司弹窗
// ---------------------------------------------------------------------------
const companyDialogVisible = ref(false)
const companyDialogTitle = ref('新增保险公司')
const companyIsEdit = ref(false)
const companySubmitLoading = ref(false)
const companyFormRef = ref()
const editingId = ref<string | null>(null)
const companyForm = reactive({
companyName: '',
companyAddress: '',
contactName: '',
contactPhone: '',
invoiceTitle: '',
taxNo: '',
bankName: '',
bankAccount: '',
tag: 1 as number,
remark: ''
})
const companyFormRules = {
companyName: [{ required: true, message: '请输入保险公司名称', trigger: 'blur' }],
tag: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
// ---------------------------------------------------------------------------
// 项目价格配置弹窗
// ---------------------------------------------------------------------------
const itemDialogVisible = ref(false)
const itemDialogTitle = ref('项目价格配置')
const currentCompanyId = ref('')
const currentCompanyName = ref('')
const itemTable = ref<any[]>([])
const itemSelected = ref<any[]>([])
const itemLoading = ref(false)
const itemPagination = reactive({ page: 1, pageSize: 10, total: 0 })
const itemFormVisible = ref(false)
const itemFormTitle = ref('新增项目')
const itemIsEdit = ref(false)
const itemSubmitLoading = ref(false)
const itemFormRef = ref()
const editingItemId = ref<string | null>(null)
const itemForm = reactive({
projectName: '',
projectFee: 0 as number | null,
status: '1',
remark: ''
})
const itemFormRules = {
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
// ---------------------------------------------------------------------------
// 查看弹窗
// ---------------------------------------------------------------------------
const viewVisible = ref(false)
const viewCompanyName = ref('')
const viewSections = ref<ViewSection[]>([])
// ---------------------------------------------------------------------------
// 主表数据加载
// ---------------------------------------------------------------------------
async function loadData() {
loading.value = true
try {
const res = await fetchInsuranceCompanyList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
})
tableData.value = (res as any)?.list || []
pagination.total = (res as any)?.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
searchForm.tag = ''
pagination.page = 1
loadData()
}
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows
}
// ---------------------------------------------------------------------------
// 保险公司弹窗操作
// ---------------------------------------------------------------------------
function resetCompanyForm() {
companyForm.companyName = ''
companyForm.companyAddress = ''
companyForm.contactName = ''
companyForm.contactPhone = ''
companyForm.invoiceTitle = ''
companyForm.taxNo = ''
companyForm.bankName = ''
companyForm.bankAccount = ''
companyForm.tag = 1
companyForm.remark = ''
editingId.value = null
}
function handleAdd() {
companyIsEdit.value = false
companyDialogTitle.value = '新增保险公司'
resetCompanyForm()
companyDialogVisible.value = true
}
async function handleEdit(row: any) {
companyIsEdit.value = true
companyDialogTitle.value = '编辑保险公司'
editingId.value = row.id
companyForm.companyName = row.companyName || ''
companyForm.companyAddress = row.companyAddress || ''
companyForm.contactName = row.contactName || ''
companyForm.contactPhone = row.contactPhone || ''
companyForm.tag = row.status != null ? Number(row.status) : 1
companyForm.remark = row.remark || ''
// 加载开票信息
try {
const invRes = await fetchInsuranceCompanyInvoices(row.id)
const invoices = (invRes as any)?.list || (invRes as any) || []
if (invoices.length > 0) {
const inv = invoices[0]
companyForm.invoiceTitle = inv.invoiceTitle || inv.companyName || ''
companyForm.taxNo = inv.taxNo || ''
companyForm.bankName = inv.bankName || ''
companyForm.bankAccount = inv.bankAccount || ''
} else {
companyForm.invoiceTitle = ''
companyForm.taxNo = ''
companyForm.bankName = ''
companyForm.bankAccount = ''
}
} catch {
companyForm.invoiceTitle = ''
companyForm.taxNo = ''
companyForm.bankName = ''
companyForm.bankAccount = ''
}
companyDialogVisible.value = true
}
async function submitCompany() {
const valid = await companyFormRef.value?.validate?.().catch(() => false)
if (!valid) return
companySubmitLoading.value = true
try {
const payload = {
companyName: companyForm.companyName.trim(),
companyAddress: companyForm.companyAddress?.trim() || undefined,
contactName: companyForm.contactName?.trim() || undefined,
contactPhone: companyForm.contactPhone?.trim() || undefined,
status: String(companyForm.tag),
remark: companyForm.remark?.trim() || undefined
}
if (companyIsEdit.value && editingId.value) {
await fetchUpdateInsuranceCompany(editingId.value, payload)
// 同步开票信息
const invRes = await fetchInsuranceCompanyInvoices(editingId.value)
const invoices = (invRes as any)?.list || (invRes as any) || []
const invoicePayload = {
companyName: companyForm.invoiceTitle?.trim() || undefined,
invoiceTitle: companyForm.invoiceTitle?.trim() || undefined,
taxNo: companyForm.taxNo?.trim() || undefined,
bankName: companyForm.bankName?.trim() || undefined,
bankAccount: companyForm.bankAccount?.trim() || undefined
}
if (invoices.length > 0) {
await fetchUpdateInsuranceCompanyInvoice(invoices[0].id, invoicePayload)
} else if (companyForm.invoiceTitle) {
await fetchCreateInsuranceCompanyInvoice(editingId.value, invoicePayload)
}
ElMessage.success('已保存')
} else {
const newInst = await fetchCreateInsuranceCompany(payload)
// 新增时同步创建开票信息
if ((newInst as any)?.id && companyForm.invoiceTitle?.trim()) {
const invoicePayload = {
companyName: companyForm.invoiceTitle?.trim() || undefined,
invoiceTitle: companyForm.invoiceTitle?.trim() || undefined,
taxNo: companyForm.taxNo?.trim() || undefined,
bankName: companyForm.bankName?.trim() || undefined,
bankAccount: companyForm.bankAccount?.trim() || undefined,
}
await fetchCreateInsuranceCompanyInvoice((newInst as any).id, invoicePayload)
}
ElMessage.success('已新增')
}
companyDialogVisible.value = false
loadData()
} finally {
companySubmitLoading.value = false
}
}
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(`确定删除「${row.companyName}」?其下属项目价格将一并删除。`, '提示', {
type: 'warning'
})
} catch {
return
}
await withFullscreenLoading(async () => {
await fetchDeleteInsuranceCompany(row.id)
ElMessage.success('已删除')
await loadData()
}, fullscreenLoadingText.delete)
}
// ---------------------------------------------------------------------------
// 查看弹窗
// ---------------------------------------------------------------------------
async function handleView(row: any) {
viewCompanyName.value = row.companyName || ''
const sections: ViewSection[] = [
{
title: '基本信息',
fields: [
{ label: '保险公司名称', value: row.companyName },
{ 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.remark, span: true }
]
}
]
// 加载开票信息
try {
const invRes = await fetchInsuranceCompanyInvoices(row.id)
const invoices = (invRes as any)?.list || (invRes as any) || []
if (invoices.length > 0) {
const inv = invoices[0]
sections.push({
title: '开票信息',
fields: [
{ label: '发票抬头', value: inv.invoiceTitle || inv.companyName },
{ label: '纳税人识别号', value: inv.taxNo },
{ label: '开户银行', value: inv.bankName },
{ label: '银行账号', value: inv.bankAccount }
]
})
}
} catch {
// ignore
}
viewSections.value = sections
viewVisible.value = true
}
// ---------------------------------------------------------------------------
// 项目价格配置弹窗
// ---------------------------------------------------------------------------
function openItemDialog(row: any) {
currentCompanyId.value = row.id
currentCompanyName.value = row.companyName || ''
itemPagination.page = 1
itemSelected.value = []
itemDialogTitle.value = `项目价格配置 — ${row.companyName}`
itemDialogVisible.value = true
loadItems()
}
async function loadItems() {
if (!currentCompanyId.value) return
itemLoading.value = true
try {
const res = await fetchInsuranceProjectPrices(currentCompanyId.value)
const list = Array.isArray(res) ? res : (res as any)?.list || []
itemTable.value = list
itemPagination.total = Array.isArray(res) ? list.length : ((res as any)?.total ?? list.length)
} finally {
itemLoading.value = false
}
}
function onItemDialogClosed() {
currentCompanyId.value = ''
currentCompanyName.value = ''
itemTable.value = []
}
function onItemSelectionChange(rows: any[]) {
itemSelected.value = rows
}
function resetItemForm() {
itemForm.projectName = ''
itemForm.projectFee = 0
itemForm.status = '1'
itemForm.remark = ''
editingItemId.value = null
}
function handleAddItem() {
itemIsEdit.value = false
itemFormTitle.value = '新增项目'
resetItemForm()
itemFormVisible.value = true
}
function handleEditItem(row: any) {
itemIsEdit.value = true
itemFormTitle.value = '编辑项目'
editingItemId.value = row.id
itemForm.projectName = row.projectName || ''
itemForm.projectFee = row.projectFee ?? 0
itemForm.status = row.status != null ? String(row.status) : '1'
itemForm.remark = row.remark || ''
itemFormVisible.value = true
}
async function submitItem() {
const valid = await itemFormRef.value?.validate?.().catch(() => false)
if (!valid) return
itemSubmitLoading.value = true
try {
const payload = {
projectName: itemForm.projectName.trim(),
projectFee: itemForm.projectFee,
status: itemForm.status,
remark: itemForm.remark?.trim() || undefined
}
if (itemIsEdit.value && editingItemId.value) {
await fetchUpdateInsuranceProjectPrice(editingItemId.value, payload)
ElMessage.success('已保存')
} else {
await fetchCreateInsuranceProjectPrice(currentCompanyId.value, payload)
ElMessage.success('已新增')
}
itemFormVisible.value = false
loadItems()
} finally {
itemSubmitLoading.value = false
}
}
async function handleDeleteItem(row: any) {
try {
await ElMessageBox.confirm(`确定删除「${row.projectName}」?`, '提示', { type: 'warning' })
} catch {
return
}
await withFullscreenLoading(async () => {
await fetchDeleteInsuranceProjectPrice(row.id)
ElMessage.success('已删除')
await loadItems()
}, fullscreenLoadingText.delete)
}
onMounted(() => loadData())
</script>
<style scoped lang="scss">
.biz-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.item-dialog-head {
margin-bottom: 12px;
}
.item-dialog-head .muted {
font-size: 13px;
color: var(--mint-text-muted);
}
.item-toolbar {
margin-bottom: 0;
}
.item-table-wrap {
padding-top: 0;
}
.form-section {
background: #fafafa;
border-radius: 8px;
padding: 20px;
margin: 4px 0;
}
.form-section .section-title {
font-size: 15px;
font-weight: 500;
color: #303133;
margin-bottom: 16px;
padding-bottom: 12px;
padding-left: 10px;
border-bottom: 1px solid #ebeef5;
border-left: 3px solid #10b981;
}
</style>
<!-- 弹窗 teleport 到 body,高度类名需非 scoped -->
<style lang="scss">
.el-dialog.insurance-item-config-dialog.dialog-form-layout {
min-height: min(640px, calc(100vh - 80px));
}
</style>
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">样本编码列表</h2>
<h2>样本编码列表</h2>
</div>
<div class="filter-block">
@@ -34,11 +34,11 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增样本编码
</el-button>
<el-button v-if="hasAuth('add')" class="btn-ghost" @click="openBatchGenerate">
<el-button class="btn-ghost" @click="openBatchGenerate">
批量生成
</el-button>
<el-button
@@ -56,7 +56,6 @@
批量收回
</el-button>
<el-button
v-if="hasAuth('delete')"
class="btn-ghost"
type="danger"
:disabled="selectedRows.length === 0"
@@ -147,8 +146,8 @@
>
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
<el-button v-if="hasAuth('edit')" class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
<el-button v-if="hasAuth('delete')" 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>
@@ -392,9 +391,7 @@
fetchBatchIssueSampleCodes,
fetchBatchRevokeSampleCodes
} from '@/api/admin'
import { useAuth } from '@/hooks/core/useAuth'
import { sampleCodeTypeLabel } from '@/utils/status-tag-registry'
const { hasAuth } = useAuth()
defineOptions({ name: 'BizSampleCode' })
@@ -0,0 +1,400 @@
<!-- 满月待办 — 保险办理管理 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<header class="page-head">
<h1>满月待办管理</h1>
<p class="page-desc">集中管理满月待办保险办理情况,支持筛选与批量办理。</p>
</header>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">待办列表</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="协议号/姓名"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="待办状态">
<el-select v-model="searchForm.todoStatus" placeholder="全部" clearable style="width: 100%">
<el-option label="全部" value="" />
<el-option label="未办保险" value="0" />
<el-option label="已办报销" value="1" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions-row">
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="protocolNo" label="采集协议号" width="160" align="center" header-align="center" />
<el-table-column prop="motherName" label="母亲姓名" width="100" align="center" header-align="center" />
<el-table-column prop="newbornDeliveryTime" label="新生儿分娩时间" width="140" align="center" header-align="center" />
<el-table-column prop="fullmoonDate" label="满月日期" width="120" align="center" header-align="center" />
<el-table-column prop="todoStatus" label="待办状态" width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.todoStatus == 1 ? 'success' : 'warning'" size="small">
{{ row.todoStatus == 1 ? '已办报销' : '未办保险' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="insuranceSubscribed" label="是否已订购保险" width="120" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.insuranceSubscribed == 1 ? 'success' : 'info'" size="small">
{{ row.insuranceSubscribed == 1 ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="insuranceCompanyName" label="保险公司" width="140" align="center" header-align="center" />
<el-table-column prop="insuranceType" label="保险险种" width="120" align="center" header-align="center" />
<el-table-column label="操作" width="160" fixed="right" align="center" header-align="center">
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleEdit(row)">办理保险</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(v) => ((pagination.page = 1), (pagination.pageSize = v), loadData())"
@current-change="(v) => ((pagination.page = v), loadData())"
/>
</div>
</div>
<!-- 办理保险弹窗 -->
<el-dialog
v-model="dialogVisible"
title="办理保险"
width="720px"
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"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="保险公司" prop="insuranceCompanyId">
<el-select
v-model="formData.insuranceCompanyId"
placeholder="请选择保险公司"
filterable
style="width: 100%"
@change="handleInsuranceCompanyChange"
>
<el-option
v-for="c in insuranceCompanyOptions"
:key="c.id"
:label="c.companyName"
:value="c.id"
/>
</el-select>
</el-form-item>
<el-form-item label="保险险种" prop="insuranceType">
<el-input v-model="formData.insuranceType" placeholder="请输入保险险种" />
</el-form-item>
<el-form-item label="保险单号">
<el-input v-model="formData.insurancePolicyNo" placeholder="请输入保险单号" />
</el-form-item>
<el-form-item label="保单费用(元)">
<el-input-number
v-model="formData.insuranceFee"
:precision="2"
:min="0"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="保险单生效时间">
<el-date-picker
v-model="formData.policyEffectiveDate"
type="date"
placeholder="请选择生效时间"
format="YYYYMMDD"
value-format="YYYYMMDD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="保险单失效时间">
<el-date-picker
v-model="formData.policyExpiryDate"
type="date"
placeholder="请选择失效时间"
format="YYYYMMDD"
value-format="YYYYMMDD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="是否已订购保险">
<el-radio-group v-model="formData.insuranceSubscribed">
<el-radio :value="1">是</el-radio>
<el-radio :value="0">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="办理日期">
<el-date-picker
v-model="formData.processDate"
type="date"
placeholder="请选择办理日期"
format="YYYYMMDD"
value-format="YYYYMMDD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="待办状态">
<el-radio-group v-model="formData.todoStatus">
<el-radio :value="0">未办保险</el-radio>
<el-radio :value="1">已办报销</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="保险单附件" class="form-col-span-2">
<el-upload
v-model:file-list="formData.attachments"
action=""
:auto-upload="false"
:limit="10"
multiple
>
<el-button type="primary" plain>选择文件</el-button>
<template #tip>
<div class="el-upload__tip">支持上传多个附件,单个文件不超过 20MB</div>
</template>
</el-upload>
</el-form-item>
<el-form-item label="办理备注" class="form-col-span-2">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入办理备注" />
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit">
<el-icon><Check /></el-icon>确定
</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Close, Check, Search, RefreshLeft } from '@element-plus/icons-vue'
import {
fetchFullmoonTodoList,
fetchUpdateFullmoonTodoInsurance,
fetchInsuranceCompanyList
} from '@/api/admin'
defineOptions({ name: 'CellStorageFullmoonTodo' })
// ---------------------------------------------------------------------------
// 搜索 & 主表
// ---------------------------------------------------------------------------
const searchForm = reactive({ keyword: '', todoStatus: '' })
const tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
// ---------------------------------------------------------------------------
// 保险公司下拉
// ---------------------------------------------------------------------------
const insuranceCompanyOptions = ref<any[]>([])
async function loadInsuranceCompanies() {
try {
const res = await fetchInsuranceCompanyList({ page: 1, pageSize: 100 })
insuranceCompanyOptions.value = (res as any)?.list || []
} catch {
insuranceCompanyOptions.value = []
}
}
// ---------------------------------------------------------------------------
// 主表数据加载
// ---------------------------------------------------------------------------
async function loadData() {
loading.value = true
try {
const res = await fetchFullmoonTodoList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
todoStatus: searchForm.todoStatus || undefined
})
tableData.value = (res as any)?.list || []
pagination.total = (res as any)?.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
searchForm.todoStatus = ''
pagination.page = 1
loadData()
}
// ---------------------------------------------------------------------------
// 办理保险弹窗
// ---------------------------------------------------------------------------
const dialogVisible = ref(false)
const submitLoading = ref(false)
const formRef = ref()
const editingRow = ref<any>(null)
const formData = reactive({
insuranceCompanyId: '' as string,
insuranceCompanyName: '' as string,
insuranceType: '' as string,
insurancePolicyNo: '' as string,
policyEffectiveDate: '' as string,
policyExpiryDate: '' as string,
insuranceSubscribed: 0 as number,
insuranceFee: 0 as number,
processDate: '' as string,
remark: '' as string,
todoStatus: 0 as number,
attachments: [] as any[]
})
const formRules = {
insuranceCompanyId: [{ required: true, message: '请选择保险公司', trigger: 'change' }],
insuranceType: [{ required: true, message: '请输入保险险种', trigger: 'blur' }]
}
function resetForm() {
formData.insuranceCompanyId = ''
formData.insuranceCompanyName = ''
formData.insuranceType = ''
formData.insurancePolicyNo = ''
formData.policyEffectiveDate = ''
formData.policyExpiryDate = ''
formData.insuranceSubscribed = 0
formData.insuranceFee = 0
formData.processDate = ''
formData.remark = ''
formData.todoStatus = 0
formData.attachments = []
editingRow.value = null
}
function handleEdit(row: any) {
editingRow.value = row
resetForm()
// 回填已有数据
formData.insuranceCompanyId = row.insuranceCompanyId || ''
formData.insuranceCompanyName = row.insuranceCompanyName || ''
formData.insuranceType = row.insuranceType || ''
formData.insurancePolicyNo = row.insurancePolicyNo || ''
formData.policyEffectiveDate = row.policyEffectiveDate || ''
formData.policyExpiryDate = row.policyExpiryDate || ''
formData.insuranceSubscribed = row.insuranceSubscribed ?? 0
formData.insuranceFee = row.insuranceFee ?? 0
formData.processDate = row.processDate || ''
formData.remark = row.remark || ''
formData.todoStatus = row.todoStatus ?? 0
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
function handleInsuranceCompanyChange(val: string) {
const found = insuranceCompanyOptions.value.find((c) => c.id === val)
formData.insuranceCompanyName = found ? found.companyName : ''
}
async function handleSubmit() {
await formRef.value?.validate?.()
if (!editingRow.value?.protocolId) return
submitLoading.value = true
try {
const payload = {
insuranceCompanyId: formData.insuranceCompanyId || undefined,
insuranceCompanyName: formData.insuranceCompanyName || undefined,
insuranceType: formData.insuranceType?.trim() || undefined,
insurancePolicyNo: formData.insurancePolicyNo?.trim() || undefined,
policyEffectiveDate: formData.policyEffectiveDate || undefined,
policyExpiryDate: formData.policyExpiryDate || undefined,
insuranceSubscribed: formData.insuranceSubscribed,
insuranceFee: formData.insuranceFee || undefined,
processDate: formData.processDate || undefined,
remark: formData.remark?.trim() || undefined,
todoStatus: formData.todoStatus
}
await fetchUpdateFullmoonTodoInsurance(editingRow.value.protocolId, payload)
ElMessage.success('保存成功')
dialogVisible.value = false
await loadData()
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
submitLoading.value = false
}
}
onMounted(() => {
loadData()
loadInsuranceCompanies()
})
</script>
<style scoped lang="scss">
.biz-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
@@ -0,0 +1,193 @@
<!-- 物料变动记录 -->
<!-- eslint-disable vue/no-v-model-argument -->
<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.changeType" placeholder="全部" clearable>
<el-option label="入库" value="rk" />
<el-option label="出库" value="ck" />
</el-select>
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker
v-model="dateRange"
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-button>
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</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-key="id"
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column
prop="materialCode"
label="物料编码"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="物料名称"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column label="变动类型" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.changeType === 'rk' ? 'success' : 'warning'" size="small">
{{ row.changeType === 'rk' ? '入库' : '出库' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="变动数量" min-width="100" align="center" header-align="center">
<template #default="{ row }">
{{ row.changeType === 'rk' ? '+' : '' }}{{ row.changeQty }}
</template>
</el-table-column>
<el-table-column
prop="stockBefore"
label="变动前库存"
min-width="110"
align="center"
header-align="center"
/>
<el-table-column
prop="stockAfter"
label="变动后库存"
min-width="110"
align="center"
header-align="center"
/>
<el-table-column label="领用人" min-width="100" align="center" header-align="center">
<template #default="{ row }">
{{ 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 || '-') : '-' }}
</template>
</el-table-column>
<el-table-column
prop="createBy"
label="操作人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="createTime"
label="操作时间"
min-width="180"
align="center"
header-align="center"
/>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { fetchMaterialStockLogList } from '@/api/material'
import type { MaterialStockLog } from '@/api/material'
defineOptions({ name: 'CellStorageMaterialStockLog' })
const searchForm = reactive({ keyword: '', changeType: '' })
const dateRange = ref<[string, string] | null>(null)
const tableData = ref<MaterialStockLog[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
async function loadData() {
loading.value = true
try {
const res = await fetchMaterialStockLogList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
changeType: searchForm.changeType || undefined,
startTime: dateRange.value?.[0] || undefined,
endTime: dateRange.value?.[1] || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
searchForm.changeType = ''
dateRange.value = null
pagination.page = 1
loadData()
}
onMounted(() => {
loadData()
})
</script>
@@ -0,0 +1,669 @@
<!-- 市场物料库 -->
<!-- eslint-disable vue/no-v-model-argument -->
<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.category" placeholder="全部" clearable>
<el-option
v-for="opt in categoryOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable>
<el-option label="正常" value="1" />
<el-option label="停用" value="0" />
</el-select>
</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-button>
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增物料
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column
prop="materialCode"
label="物料编码"
min-width="130"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="物料名称"
min-width="150"
align="center"
header-align="center"
/>
<el-table-column label="物料分类" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="getCategoryTagType(row.category)" size="small">
{{ getCategoryLabel(row.category) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="specModel"
label="规格型号"
min-width="120"
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="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 label="操作" min-width="260" fixed="right" align="center" header-align="center">
<template #default="{ row }">
<el-button class="btn-op-item" @click="handleStockIn(row)">入库</el-button>
<el-button class="btn-op-item" @click="handleStockOut(row)">出库</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>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50]"
layout="total, sizes, prev, pager, next"
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
<!-- 新增/编辑弹窗 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="560px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="material-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="物料编码" prop="materialCode">
<el-input v-model="formData.materialCode" placeholder="请输入物料编码" />
</el-form-item>
<el-form-item label="物料名称" prop="materialName">
<el-input v-model="formData.materialName" placeholder="请输入物料名称" />
</el-form-item>
<el-form-item label="物料分类" prop="category">
<el-select v-model="formData.category" placeholder="请选择物料分类" clearable>
<el-option
v-for="opt in categoryOptions"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="规格型号" prop="specModel">
<el-input v-model="formData.specModel" placeholder="请输入规格型号" />
</el-form-item>
<el-form-item label="单位" prop="unit">
<el-input v-model="formData.unit" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio value="1">正常</el-radio>
<el-radio value="0">停用</el-radio>
</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-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="dialogVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
<el-icon><Check /></el-icon>
确定
</el-button>
</template>
</el-dialog>
<!-- 入库弹窗 -->
<el-dialog
v-model="stockInVisible"
title="物料入库"
width="480px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="stockInFormRef"
:model="stockInForm"
:rules="stockInRules"
label-position="top"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<el-form-item label="入库数量" prop="qty">
<el-input-number
v-model="stockInForm.qty"
:min="0.01"
:precision="2"
:step="1"
placeholder="请输入入库数量"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="stockInForm.remark"
type="textarea"
:rows="3"
placeholder="可选"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="stockInVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button
class="btn-main"
type="primary"
@click="handleStockInSubmit"
:loading="stockInLoading"
>
<el-icon><Check /></el-icon>
确认入库
</el-button>
</template>
</el-dialog>
<!-- 出库弹窗 -->
<el-dialog
v-model="stockOutVisible"
title="物料出库"
width="480px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="stockOutFormRef"
:model="stockOutForm"
:rules="stockOutRules"
label-position="top"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<el-form-item label="出库数量" prop="qty">
<el-input-number
v-model="stockOutForm.qty"
:min="0.01"
:precision="2"
:step="1"
placeholder="请输入出库数量"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="领用人" prop="receiver">
<el-input v-model="stockOutForm.receiver" placeholder="请输入领用人" />
</el-form-item>
<el-form-item label="用途">
<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-form-item>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="stockOutVisible = false">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button
class="btn-main"
type="primary"
@click="handleStockOutSubmit"
:loading="stockOutLoading"
>
<el-icon><Check /></el-icon>
确认出库
</el-button>
</template>
</el-dialog>
<!-- 查看弹窗 -->
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewRow?.materialName || '物料详情'"
:icon="Box"
:meta="viewRow?.createTime || ''"
:sections="viewSections"
/>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Box } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { fetchDictDataOptions } from '@/api/admin'
import {
fetchMaterialList,
fetchCreateMaterial,
fetchUpdateMaterial,
fetchDeleteMaterial,
fetchStockInMaterial,
fetchStockOutMaterial
} from '@/api/material'
import type { Material } from '@/api/material'
defineOptions({ name: 'CellStorageMaterial' })
const searchForm = reactive({ keyword: '', category: '', status: '' })
const tableData = ref<Material[]>([])
const selectedRows = ref<Material[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
// 字典选项
const categoryOptions = ref<{ label: string; value: string }[]>([])
// 新增/编辑
const dialogVisible = ref(false)
const dialogTitle = ref('新增物料')
const isEdit = ref(false)
const submitLoading = ref(false)
const formRef = ref()
const editingId = ref<string | null>(null)
const formData = reactive({
materialCode: '',
materialName: '',
category: '',
specModel: '',
unit: '',
status: '1',
remark: ''
})
const formRules = {
materialCode: [{ required: true, message: '请输入物料编码', trigger: 'blur' }],
materialName: [{ required: true, message: '请输入物料名称', trigger: 'blur' }]
}
// 入库
const stockInVisible = ref(false)
const stockInLoading = ref(false)
const stockInFormRef = ref()
const stockInMaterialId = ref<string | null>(null)
const stockInForm = reactive({ qty: 1, remark: '' })
const stockInRules = {
qty: [{ required: true, message: '请输入入库数量', trigger: 'blur' }]
}
// 出库
const stockOutVisible = ref(false)
const stockOutLoading = ref(false)
const stockOutFormRef = ref()
const stockOutMaterialId = ref<string | null>(null)
const stockOutStockQty = ref(0)
const stockOutForm = reactive({ qty: 1, receiver: '', purpose: '', remark: '' })
const stockOutRules = {
qty: [{ required: true, message: '请输入出库数量', trigger: 'blur' }],
receiver: [{ required: true, message: '请输入领用人', trigger: 'blur' }]
}
// 查看
const viewVisible = ref(false)
const viewRow = ref<Material | null>(null)
const getCategoryLabel = (code: string) => {
if (!code) return '-'
const opt = categoryOptions.value.find((o) => o.value === code)
return opt ? opt.label : code
}
const getCategoryTagType = (code: string) => {
if (code === 'xccyw') return 'primary'
if (code === 'ccyw') return 'success'
return 'info'
}
const handleSelectionChange = (rows: Material[]) => {
selectedRows.value = rows
}
const viewSections = computed(() => {
if (!viewRow.value) return []
const row = viewRow.value
return [
{
title: '基本信息',
fields: [
{ label: '物料编码', value: row.materialCode },
{ label: '物料名称', value: row.materialName },
{
label: '物料分类',
value: getCategoryLabel(row.category),
tagType: getCategoryTagType(row.category) as 'success' | 'warning' | 'danger' | 'info' | '' | 'primary'
},
{ label: '规格型号', value: row.specModel },
{ label: '单位', value: row.unit },
{
label: '状态',
value: row.status === '1' ? '正常' : '停用',
tagType: (row.status === '1' ? 'success' : 'info') as 'success' | 'info'
},
{ label: '当前库存', value: String(row.stockQty ?? 0) }
]
},
{
title: '其他信息',
fields: [
{ label: '备注', value: row.remark, span: true },
{ label: '创建时间', value: row.createTime },
{ label: '更新时间', value: row.updateTime }
]
}
]
})
async function loadDictOptions() {
try {
const res = await fetchDictDataOptions('wllb')
categoryOptions.value = res || []
} catch {
categoryOptions.value = []
}
}
async function loadData() {
loading.value = true
try {
const res = await fetchMaterialList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
category: searchForm.category || undefined,
status: searchForm.status || undefined
})
tableData.value = res.list || []
pagination.total = res.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
searchForm.category = ''
searchForm.status = ''
pagination.page = 1
loadData()
}
function resetForm() {
formData.materialCode = ''
formData.materialName = ''
formData.category = ''
formData.specModel = ''
formData.unit = ''
formData.status = '1'
formData.remark = ''
editingId.value = null
}
function handleAdd() {
isEdit.value = false
dialogTitle.value = '新增物料'
resetForm()
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
function handleEdit(row: Material) {
isEdit.value = true
dialogTitle.value = '编辑物料'
editingId.value = row.id
formData.materialCode = row.materialCode || ''
formData.materialName = row.materialName || ''
formData.category = row.category || ''
formData.specModel = row.specModel || ''
formData.unit = row.unit || ''
formData.status = row.status || '1'
formData.remark = row.remark || ''
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
async function handleSubmit() {
await formRef.value?.validate?.()
submitLoading.value = true
try {
const payload = {
materialCode: formData.materialCode.trim(),
materialName: formData.materialName.trim(),
category: formData.category || undefined,
specModel: formData.specModel?.trim() || undefined,
unit: formData.unit?.trim() || undefined,
status: formData.status,
remark: formData.remark?.trim() || undefined
}
if (isEdit.value && editingId.value) {
await fetchUpdateMaterial(editingId.value, payload)
} else {
await fetchCreateMaterial(payload)
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
await loadData()
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
submitLoading.value = false
}
}
function handleView(row: Material) {
viewRow.value = row
viewVisible.value = true
}
async function handleDelete(row: Material) {
await ElMessageBox.confirm('确定要删除该物料吗?', '提示', { type: 'warning' })
await fetchDeleteMaterial(row.id)
ElMessage.success('删除成功')
await loadData()
}
function handleStockIn(row: Material) {
stockInMaterialId.value = row.id
stockInForm.qty = 1
stockInForm.remark = ''
stockInVisible.value = true
stockInFormRef.value?.clearValidate?.()
}
async function handleStockInSubmit() {
await stockInFormRef.value?.validate?.()
stockInLoading.value = true
try {
await fetchStockInMaterial(stockInMaterialId.value!, {
qty: stockInForm.qty,
remark: stockInForm.remark?.trim() || undefined
})
ElMessage.success('入库成功')
stockInVisible.value = false
await loadData()
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
stockInLoading.value = false
}
}
function handleStockOut(row: Material) {
stockOutMaterialId.value = row.id
stockOutStockQty.value = row.stockQty ?? 0
stockOutForm.qty = 1
stockOutForm.receiver = ''
stockOutForm.purpose = ''
stockOutForm.remark = ''
stockOutVisible.value = true
stockOutFormRef.value?.clearValidate?.()
}
async function handleStockOutSubmit() {
await stockOutFormRef.value?.validate?.()
if (stockOutForm.qty > stockOutStockQty.value) {
ElMessage.warning(`出库数量不能超过当前库存 (${stockOutStockQty.value})`)
return
}
stockOutLoading.value = true
try {
await fetchStockOutMaterial(stockOutMaterialId.value!, {
qty: stockOutForm.qty,
receiver: stockOutForm.receiver?.trim() || undefined,
purpose: stockOutForm.purpose?.trim() || undefined,
remark: stockOutForm.remark?.trim() || undefined
})
ElMessage.success('出库成功')
stockOutVisible.value = false
await loadData()
} catch {
/* 校验或接口错误已由全局处理 */
} finally {
stockOutLoading.value = false
}
}
onMounted(() => {
loadDictOptions()
loadData()
})
</script>
<style scoped lang="scss">
.material-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
</style>
@@ -27,7 +27,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">缴费确认列表</h2>
<h2>缴费确认列表</h2>
</div>
<div class="list-body list-wrap payment-confirm-table-wrap">
@@ -25,7 +25,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">缴费信息列表</h2>
<h2>缴费信息列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">协议单列表</h2>
<h2>协议单列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">协议单列表</h2>
<h2>协议单列表</h2>
</div>
<div class="filter-block">
@@ -27,7 +27,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">退费确认列表</h2>
<h2>退费确认列表</h2>
</div>
<div class="list-body list-wrap refund-confirm-table-wrap">
@@ -0,0 +1,460 @@
<!-- 储存证管理 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<header class="page-head">
<h1>储存证管理</h1>
<p class="page-desc">管理细胞储存协议的储存证制作与查看。</p>
</header>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">储存证列表</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="关键词">
<el-input v-model="searchForm.keyword" placeholder="协议号/姓名" clearable />
</el-form-item>
<el-form-item label="制作状态">
<el-select v-model="searchForm.makeStatus" placeholder="全部" clearable style="width: 100%">
<el-option label="未制作" value="0" />
<el-option label="已制作" value="1" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions-row">
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="protocolNo" label="采集协议号" width="160" align="center" header-align="center" />
<el-table-column prop="motherName" label="母亲姓名" width="100" align="center" header-align="center" />
<el-table-column prop="newbornDeliveryTime" label="新生儿分娩时间" width="140" align="center" header-align="center" />
<el-table-column prop="sampleInfo" label="样本信息" min-width="180" align="center" header-align="center" show-overflow-tooltip />
<el-table-column prop="storageInDate" label="入库日期" width="120" align="center" header-align="center" />
<el-table-column prop="makeStatus" label="制作状态" 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="制作日期" width="120" align="center" header-align="center" />
<el-table-column label="操作" width="200" 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>上传储存证
</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="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
@current-change="(val) => ((pagination.page = val), loadData())"
/>
</div>
</div>
<!-- 上传储存证弹窗 -->
<el-dialog
v-model="uploadDialogVisible"
title="上传储存证"
width="640px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form
ref="uploadFormRef"
:model="uploadForm"
:rules="uploadFormRules"
label-position="top"
class="storage-cert-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<el-form-item label="采集协议号">
<el-input :model-value="uploadRow?.protocolNo" disabled />
</el-form-item>
<el-form-item label="母亲姓名">
<el-input :model-value="uploadRow?.motherName" disabled />
</el-form-item>
</div>
<el-form-item label="储存证图片" prop="certImageUrls">
<el-upload
v-model:file-list="uploadFileList"
action=""
:auto-upload="false"
list-type="picture-card"
accept="image/*"
:limit="9"
:on-exceed="() => ElMessage.warning('最多上传 9 张图片')"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<div class="form-grid-2">
<el-form-item label="制作日期" prop="makeDate">
<el-date-picker
v-model="uploadForm.makeDate"
type="date"
format="YYYYMMDD"
value-format="YYYYMMDD"
placeholder="选择日期"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="uploadForm.remark" type="textarea" :rows="2" placeholder="可选" />
</el-form-item>
</div>
</el-form>
<template #footer>
<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-icon><Check /></el-icon>确定
</el-button>
</template>
</el-dialog>
<!-- 查看弹窗 -->
<el-dialog
v-model="viewDialogVisible"
title="查看储存证"
width="720px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<div class="view-info-grid">
<div class="view-info-item">
<span class="view-label">采集协议号</span>
<span class="view-value">{{ viewRow?.protocolNo || '—' }}</span>
</div>
<div class="view-info-item">
<span class="view-label">母亲姓名</span>
<span class="view-value">{{ viewRow?.motherName || '—' }}</span>
</div>
<div class="view-info-item">
<span class="view-label">新生儿分娩时间</span>
<span class="view-value">{{ viewRow?.newbornDeliveryTime || '—' }}</span>
</div>
<div class="view-info-item">
<span class="view-label">样本信息</span>
<span class="view-value">{{ viewRow?.sampleInfo || '—' }}</span>
</div>
<div class="view-info-item">
<span class="view-label">入库日期</span>
<span class="view-value">{{ viewRow?.storageInDate || '—' }}</span>
</div>
<div class="view-info-item">
<span class="view-label">制作状态</span>
<span class="view-value">
<el-tag :type="viewRow?.makeStatus == '1' ? 'success' : 'warning'" size="small">
{{ viewRow?.makeStatus == '1' ? '已制作' : '未制作' }}
</el-tag>
</span>
</div>
<div class="view-info-item">
<span class="view-label">制作日期</span>
<span class="view-value">{{ viewRow?.makeDate || '—' }}</span>
</div>
<div class="view-info-item">
<span class="view-label">备注</span>
<span class="view-value">{{ viewRow?.remark || '—' }}</span>
</div>
</div>
<div v-if="viewCertImages.length" class="view-cert-images">
<div class="view-cert-label">储存证图片</div>
<div class="view-cert-grid">
<el-image
v-for="(url, idx) in viewCertImages"
:key="idx"
:src="url"
:preview-src-list="viewCertImages"
:initial-index="idx"
fit="cover"
class="view-cert-img"
/>
</div>
</div>
<template #footer>
<el-button class="btn-ghost" @click="viewDialogVisible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import type { UploadUserFile } from 'element-plus'
import {
Plus,
Close,
Check,
Search,
RefreshLeft,
View,
Upload
} from '@element-plus/icons-vue'
import { fetchStorageCertList } from '@/api/admin'
import { fetchUpdateCollectionProtocol } from '@/api/collection-protocol'
import { fetchUploadAttachment } from '@/api/business'
defineOptions({ name: 'CellStorageStorageCert' })
// ---------------------------------------------------------------------------
// 搜索 & 主表
// ---------------------------------------------------------------------------
const searchForm = reactive({ keyword: '', makeStatus: '' })
const tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
// ---------------------------------------------------------------------------
// 上传弹窗
// ---------------------------------------------------------------------------
const uploadDialogVisible = ref(false)
const uploadRow = ref<any>(null)
const uploadFormRef = ref()
const uploadFileList = ref<UploadUserFile[]>([])
const submitLoading = ref(false)
const uploadForm = reactive({
makeDate: '',
remark: ''
})
const uploadFormRules = {
certImageUrls: [{ required: true, message: '请上传储存证图片', trigger: 'change' }],
makeDate: [{ required: true, message: '请选择制作日期', trigger: 'change' }]
}
// ---------------------------------------------------------------------------
// 查看弹窗
// ---------------------------------------------------------------------------
const viewDialogVisible = ref(false)
const viewRow = ref<any>(null)
const viewCertImages = computed(() => {
if (!viewRow.value?.certImageUrls) return []
return viewRow.value.certImageUrls
.split(',')
.map((u: string) => u.trim())
.filter(Boolean)
})
// ---------------------------------------------------------------------------
// 数据加载
// ---------------------------------------------------------------------------
async function loadData() {
loading.value = true
try {
const res = await fetchStorageCertList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
makeStatus: searchForm.makeStatus || undefined
})
tableData.value = (res as any)?.list || []
pagination.total = (res as any)?.total ?? 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
loadData()
}
function handleReset() {
searchForm.keyword = ''
searchForm.makeStatus = ''
pagination.page = 1
loadData()
}
// ---------------------------------------------------------------------------
// 上传储存证
// ---------------------------------------------------------------------------
function handleUpload(row: any) {
uploadRow.value = row
uploadFileList.value = []
uploadForm.makeDate = getTodayStr()
uploadForm.remark = ''
uploadDialogVisible.value = true
}
async function handleSubmitUpload() {
// 手动校验图片
if (uploadFileList.value.length === 0) {
ElMessage.warning('请上传储存证图片')
return
}
const valid = await uploadFormRef.value?.validate?.().catch(() => false)
if (!valid) return
submitLoading.value = true
try {
// 1. 逐个上传图片文件
const uploadedUrls: string[] = []
for (const f of uploadFileList.value) {
const file = f.raw
if (!file) continue
const fd = new FormData()
fd.append('business_type', 'storage_cert')
fd.append('business_id', uploadRow.value?.protocolId || 'temp')
fd.append('file', file)
const res: any = await fetchUploadAttachment(fd)
if (res?.fileUrl) {
uploadedUrls.push(res.fileUrl)
}
}
const existingUrls = uploadRow.value?.certImageUrls || ''
const finalUrls = [existingUrls, ...uploadedUrls].filter(Boolean).join(',')
// 2. 调用协议更新接口保存储存证数据
await fetchUpdateCollectionProtocol(uploadRow.value.protocolId, {
storageCert: {
makeStatus: '1',
certImageUrls: finalUrls,
makeDate: uploadForm.makeDate,
remark: uploadForm.remark?.trim() || undefined
}
})
ElMessage.success('上传成功')
uploadDialogVisible.value = false
await loadData()
} catch {
/* 全局已处理 */
} finally {
submitLoading.value = false
}
}
// ---------------------------------------------------------------------------
// 查看储存证
// ---------------------------------------------------------------------------
function handleView(row: any) {
viewRow.value = row
viewDialogVisible.value = true
}
// ---------------------------------------------------------------------------
// 工具函数
// ---------------------------------------------------------------------------
function getTodayStr(): string {
const d = new Date()
const yyyy = d.getFullYear()
const mm = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
return `${yyyy}${mm}${dd}`
}
onMounted(() => {
loadData()
})
</script>
<style scoped lang="scss">
.storage-cert-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.view-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px 24px;
margin-bottom: 20px;
}
.view-info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.view-label {
font-size: 12px;
color: var(--mint-text-muted, #909399);
}
.view-value {
font-size: 14px;
color: var(--el-text-color-primary, #303133);
word-break: break-all;
}
.view-cert-images {
border-top: 1px solid var(--el-border-color-lighter, #ebeef5);
padding-top: 16px;
}
.view-cert-label {
font-size: 13px;
color: var(--mint-text-muted, #909399);
margin-bottom: 12px;
}
.view-cert-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.view-cert-img {
width: 120px;
height: 96px;
border-radius: 6px;
border: 1px solid var(--el-border-color-lighter, #ebeef5);
cursor: pointer;
}
</style>
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">快速入口</h2>
<h2>快速入口</h2>
</div>
<div class="console-lite__body">
<p class="console-lite__hint">详见仓库根目录 `.cursorrules` 中「页面级 UI 模板(固定五页)」。</p>
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">洁净室列表</h2>
<h2>洁净室列表</h2>
</div>
<div class="filter-block">
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">选择项</h2>
<h2>选择项</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">预约列表</h2>
<h2>预约列表</h2>
</div>
<div class="filter-block">
@@ -12,7 +12,7 @@
<!-- 左侧:库位树 -->
<div class="w-[280px] shrink-0 border-r border-[var(--mint-border)] pr-4">
<div class="panel-top storage-aside__head">
<h2 class="title-bar-heading">库位树</h2>
<h2>库位树</h2>
<el-button type="primary" size="small" @click="handleAddRoot">
<el-icon><Plus /></el-icon>
新增
@@ -62,7 +62,7 @@
<template v-else-if="selectedNode.isLeaf !== 1">
<div class="storage-content-header">
<div>
<h3 class="title-bar-heading">
<h3>
{{
buildTreePathLabelFromFlat(selectedNode.id, flatList, { nameKey: 'nodeName' })
}}
@@ -76,7 +76,7 @@
<template v-else>
<div class="storage-content-header">
<div>
<h3 class="title-bar-heading">
<h3>
{{
buildTreePathLabelFromFlat(selectedNode.id, flatList, { nameKey: 'nodeName' })
}}
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">价格列表</h2>
<h2>价格列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">规格列表</h2>
<h2>规格列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">类型列表</h2>
<h2>类型列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">渠道列表</h2>
<h2>渠道列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">机构列表</h2>
<h2>机构列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">体系列表</h2>
<h2>体系列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">产品列表</h2>
<h2>产品列表</h2>
</div>
<div class="filter-block">
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">耗材包列表</h2>
<h2>耗材包列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">耗材列表</h2>
<h2>耗材列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">类型列表</h2>
<h2>类型列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">分类列表</h2>
<h2>分类列表</h2>
</div>
<div class="filter-block">
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">路线列表</h2>
<h2>路线列表</h2>
</div>
<div class="filter-block">
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">工艺列表</h2>
<h2>工艺列表</h2>
</div>
<div class="filter-block">
@@ -11,7 +11,7 @@
<!-- ========== 列表视图 ========== -->
<template v-if="currentView === 'list'">
<div class="panel-top">
<h2 class="title-bar-heading">任务列表</h2>
<h2>任务列表</h2>
</div>
<div class="filter-block">
@@ -95,7 +95,7 @@
<div class="task-detail-header">
<div class="task-detail-header-info">
<div class="task-detail-title-row">
<h2 class="title-bar-heading">任务执行</h2>
<h2>任务执行</h2>
<el-tag v-if="taskDetail" :type="taskStatusTag(taskDetail.taskStatus).type" size="large">
{{ taskStatusTag(taskDetail.taskStatus).label }}
</el-tag>
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">工单列表</h2>
<h2>工单列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">试剂列表</h2>
<h2>试剂列表</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">SOP列表</h2>
<h2>SOP列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">项目列表</h2>
<h2>项目列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">工单列表</h2>
<h2>工单列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">接收检查项列表</h2>
<h2>接收检查项列表</h2>
</div>
<div class="filter-block">
@@ -27,7 +27,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">样本接收列表</h2>
<h2>样本接收列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">公司列表</h2>
<h2>公司列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">捐赠协议单列表</h2>
<h2>捐赠协议单列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">捐赠协议单列表</h2>
<h2>捐赠协议单列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">分类列表</h2>
<h2>分类列表</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">组织管理</h2>
<h2>组织管理</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">字典类型</h2>
<h2>字典类型</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">快递公司列表</h2>
<h2>快递公司列表</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">请假列表</h2>
<h2>请假列表</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">菜单列表</h2>
<h2>菜单列表</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -24,7 +24,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">订单列表</h2>
<h2>订单列表</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">职位列表</h2>
<h2>职位列表</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -8,7 +8,7 @@
</header>
<div class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">商品列表</h2>
<h2>商品列表</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
+1 -1
View File
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">角色列表</h2>
<h2>角色列表</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">站点列表</h2>
<h2>站点列表</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -13,7 +13,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">租户列表</h2>
<h2>租户列表</h2>
</div>
<div class="filter-block">
+1 -1
View File
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">单位列表</h2>
<h2>单位列表</h2>
</div>
<div class="filter-block">
+2 -2
View File
@@ -13,7 +13,7 @@
<div class="w-[280px] shrink-0 border-r border-[var(--mint-border)] pr-4">
<!-- 标题与搜索同处 panel-top:避免标题栏底边与独立输入框之间出现不可控的视觉断层 -->
<div class="panel-top user-dept-aside__head">
<h2 class="title-bar-heading">组织</h2>
<h2>组织</h2>
<el-input v-model="deptKeyword" placeholder="搜索组织" clearable />
</div>
<el-tree
@@ -32,7 +32,7 @@
<!-- 右侧:用户列表 -->
<div class="flex-1 min-w-0">
<div class="panel-top">
<h2 class="title-bar-heading">用户列表</h2>
<h2>用户列表</h2>
</div>
<div class="filter-block">
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">申请列表</h2>
<h2>申请列表</h2>
</div>
<div class="filter-block">
@@ -9,7 +9,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">流程列表</h2>
<h2>流程列表</h2>
</div>
<div class="filter-block">
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">已办列表</h2>
<h2>已办列表</h2>
</div>
<div class="filter-block">
@@ -8,7 +8,7 @@
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">待办列表</h2>
<h2>待办列表</h2>
</div>
<div class="filter-block">