This commit is contained in:
zlwu committed 2026-05-20 14:32:07 +08:00
1 parent 15ed25e33b
commit dc31968204
143 files changed
+15823 -9989

No files matched your search

+2 -2
View File
@@ -1728,7 +1728,7 @@ export function fetchDeleteCheckupInstitutionPackage(packageId: string) {
return request.del({ url: `/web/admin/checkup-institution-packages/${packageId}` })
}
// ============ 保险公司管理 ============
// ============ 保险公司及险种价格管理 ============
export function fetchInsuranceCompanyList(params?: {
page?: number
@@ -1767,7 +1767,7 @@ export function fetchDeleteInsuranceCompanyInvoice(id: string) {
return request.del({ url: `/web/admin/insurance-company-invoices/${id}` })
}
// 保险项目价格
// 保险险种价格
export function fetchInsuranceProjectPrices(companyId: string) {
return request.get({ url: `/web/admin/insurance-companies/${companyId}/project-prices` })
}
+1
View File
@@ -32,6 +32,7 @@ export function fetchCollectionProtocolDetail(id: string) {
payment: Record<string, unknown> | null
paymentDetails: Array<Record<string, unknown>> | null
attachments?: Array<Record<string, unknown>>
storageCert?: Record<string, unknown> | null
}>({
url: `/web/admin/collection-protocols/${id}`
})
+21
View File
@@ -47,6 +47,27 @@ export type SampleReceptionCheckResult = {
orderNum?: number
}
export function fetchSampleReceptionUpdate(
receptionId: string,
data: {
receptionTime: string
handlerUserId?: string
handlerName?: string
receiveTemperature?: string
volumeMl?: number
netWeightG?: number
checks: Array<{ checkItemId: string; checked: boolean }>
acceptConclusion: 'js' | 'bj'
receiveAttachments?: string
remark?: string
}
) {
return request.put({
url: `/web/admin/sample-receptions/${receptionId}`,
data
})
}
export function fetchSampleReceptionSubmit(data: {
protocolSampleId?: string
sourceType?: 'cell_storage' | 'research_donation'
@@ -678,6 +678,49 @@ body .el-table thead {
}
// -------------------------------- el-table 全局样式 end --------------------------------
// border 模式:仅保留表头列之间的分割线以支持拖拽调整列宽
// 覆盖 Element Plus el-table--border 的所有默认边框
.page-container .el-table--border,
.console-container .el-table--border,
.el-dialog .el-table--border {
// 数据行:移除纵向边框
td.el-table__cell {
border-right: none !important;
}
// 表头:仅列之间保留淡色分割线,去掉底部边框
th.el-table__cell {
border-right: 1px solid var(--mint-border) !important;
border-top: none !important;
}
th.el-table__cell:first-child {
border-left: none !important;
}
th.el-table__cell:last-child {
border-right: none !important;
}
// 移除表格外边框(左 ::before、右 ::after)
&::after,
&::before {
content: none !important;
}
// 移除表格顶部边框
.el-table__inner-wrapper::after {
content: none !important;
}
// 移除边框补丁元素
.el-table__border-left-patch,
.el-table__border-bottom-patch,
.el-table__border-right-patch {
display: none !important;
}
}
.page-container .el-table,
.console-container .el-table {
--el-table-header-bg-color: var(--mint-table-head);
@@ -180,6 +180,14 @@
border-top: 1px dashed #e2e8f0;
}
.sample-card__section-label {
font-size: 11px;
font-weight: 600;
color: #94a3b8;
margin-bottom: 10px;
letter-spacing: 0.3px;
}
.sample-qc-empty,
.sample-qc-hint {
font-size: 13px;
@@ -216,10 +224,6 @@
border-radius: 8px;
background: #fff;
overflow: hidden;
&--task {
border-left: 3px solid #0ea5e9;
}
}
.qc-detail-card__header {
@@ -357,6 +357,10 @@
// 标签页操作逻辑
const useTabOperations = (adjustPositionAfterClose: () => void) => {
const clickTab = (item: WorkTab) => {
if (item.path === activeTab.value) {
useCommon().refresh()
return
}
router.push({
path: item.path,
query: item.query as LocationQueryRaw
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_sample_qc_info_cards_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/business/sample-qc-info-cards/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_payment_info_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/payment-info/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_material_stock_log_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material-stock-log/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_material_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L1"}, {"id": "material_index_getcategorylabel", "label": "getCategoryLabel()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L432"}, {"id": "material_index_getcategorytagtype", "label": "getCategoryTagType()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L438"}, {"id": "material_index_handleselectionchange", "label": "handleSelectionChange()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L444"}, {"id": "material_index_loaddictoptions", "label": "loadDictOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L483"}, {"id": "material_index_loaddata", "label": "loadData()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L492"}, {"id": "material_index_handlesearch", "label": "handleSearch()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L509"}, {"id": "material_index_handlereset", "label": "handleReset()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L514"}, {"id": "material_index_resetform", "label": "resetForm()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L522"}, {"id": "material_index_handleadd", "label": "handleAdd()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L533"}, {"id": "material_index_handleedit", "label": "handleEdit()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L541"}, {"id": "material_index_handlesubmit", "label": "handleSubmit()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L556"}, {"id": "material_index_handleview", "label": "handleView()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L584"}, {"id": "material_index_handledelete", "label": "handleDelete()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L589"}, {"id": "material_index_handlestockin", "label": "handleStockIn()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L596"}, {"id": "material_index_handlestockinsubmit", "label": "handleStockInSubmit()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L604"}, {"id": "material_index_handlestockout", "label": "handleStockOut()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L622"}, {"id": "material_index_handlestockoutsubmit", "label": "handleStockOutSubmit()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L633"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_material_index_vue", "target": "element_plus", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L356", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_material_index_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/material/index.vue", "source_location": "L357", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_material_index_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_system_view_dialog_index_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/vLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "label": "DonationViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L1"}, {"id": "list_donationviewdialog_dash", "label": "dash()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L684"}, {"id": "list_donationviewdialog_formatymd", "label": "formatYmd()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L689"}, {"id": "list_donationviewdialog_formatym", "label": "formatYm()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L695"}, {"id": "list_donationviewdialog_auditordisplay", "label": "auditorDisplay()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L701"}, {"id": "list_donationviewdialog_sampletypename", "label": "sampleTypeName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L707"}, {"id": "list_donationviewdialog_unitname", "label": "unitName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L711"}, {"id": "list_donationviewdialog_sitename", "label": "siteName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L715"}, {"id": "list_donationviewdialog_expressname", "label": "expressName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L719"}, {"id": "list_donationviewdialog_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L724"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L512", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolattachmentreadonly_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L513", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_sample_qc_info_cards_index_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L514", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_research_donation", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L515", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L520", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_download_attachment", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L529", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_basic_info_insurance_company_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/basic-info/insurance-company/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "label": "ProtocolViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1"}, {"id": "protocol_protocolviewdialog_dash", "label": "dash()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L284"}, {"id": "protocol_protocolviewdialog_handleclosed", "label": "handleClosed()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1583"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "target": "protocol_protocolviewdialog_dash", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L284", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "target": "protocol_protocolviewdialog_handleclosed", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1583", "weight": 1.0}], "raw_calls": [{"caller_nid": "protocol_protocolviewdialog_dash", "callee": "unitName", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L284"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_api_collection_protocol_ts", "label": "collection-protocol.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L1"}, {"id": "api_collection_protocol_fetchcollectionprotocollist", "label": "fetchCollectionProtocolList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L14"}, {"id": "api_collection_protocol_fetchcollectionprotocoldetail", "label": "fetchCollectionProtocolDetail()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L26"}, {"id": "api_collection_protocol_fetchcollectionprotocolauditlist", "label": "fetchCollectionProtocolAuditList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L41"}, {"id": "api_collection_protocol_fetchsubmitprotocolaudit", "label": "fetchSubmitProtocolAudit()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L53"}, {"id": "api_collection_protocol_fetchreceiptconfirmlist", "label": "fetchReceiptConfirmList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L65"}, {"id": "api_collection_protocol_fetchreceiptconfirmedlist", "label": "fetchReceiptConfirmedList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L72"}, {"id": "api_collection_protocol_fetchreceiptconfirmsubmit", "label": "fetchReceiptConfirmSubmit()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L79"}, {"id": "api_collection_protocol_fetchrefundconfirmlist", "label": "fetchRefundConfirmList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L90"}, {"id": "api_collection_protocol_fetchrefundconfirmedlist", "label": "fetchRefundConfirmedList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L97"}, {"id": "api_collection_protocol_fetchrefundconfirmsubmit", "label": "fetchRefundConfirmSubmit()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L104"}, {"id": "api_collection_protocol_fetchsubmitreceiptfinance", "label": "fetchSubmitReceiptFinance()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L115"}, {"id": "api_collection_protocol_fetchsubmitreceiptfinancebatch", "label": "fetchSubmitReceiptFinanceBatch()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L122"}, {"id": "api_collection_protocol_fetchsubmitrefundfinance", "label": "fetchSubmitRefundFinance()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L130"}, {"id": "api_collection_protocol_fetchsubmitrefundfinancebatch", "label": "fetchSubmitRefundFinanceBatch()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L145"}, {"id": "api_collection_protocol_fetchpaymentdetaillist", "label": "fetchPaymentDetailList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L158"}, {"id": "api_collection_protocol_fetchcreatecollectionprotocol", "label": "fetchCreateCollectionProtocol()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L171"}, {"id": "api_collection_protocol_fetchupdatecollectionprotocol", "label": "fetchUpdateCollectionProtocol()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L179"}, {"id": "api_collection_protocol_fetchdeletecollectionprotocol", "label": "fetchDeleteCollectionProtocol()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L190"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_api_collection_protocol_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_http", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/collection-protocol.ts", "source_location": "L1", "weight": 1.0}, {"Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_material_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "label": "ProtocolViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1"}, {"id": "protocol_protocolviewdialog_dash", "label": "dash()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L310"}, {"id": "protocol_protocolviewdialog_handleclosed", "label": "handleClosed()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1613"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "target": "protocol_protocolviewdialog_dash", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L310", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolviewdialog_vue", "target": "protocol_protocolviewdialog_handleclosed", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L1613", "weight": 1.0}], "raw_calls": [{"caller_nid": "protocol_protocolviewdialog_dash", "callee": "unitName", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue", "source_location": "L310"}]}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_fullmoon_todo_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/fullmoon-todo/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_types_import_components_d_ts", "label": "components.d.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/types/import/components.d.ts", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin_ts", "label": "admin.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L1"}, {"id": "api_admin_fetchgetmenulist", "label": "fetchGetMenuList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L10"}, {"id": "api_admin_fetchgetrolelist", "label": "fetchGetRoleList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L19"}, {"id": "api_admin_fetchuseroptions", "label": "fetchUserOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L26"}, {"id": "api_admin_fetchroleoptions", "label": "fetchRoleOptions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L33"}, {"id": "api_admin_fetchtenantlist", "label": "fetchTenantList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L43"}, {"id": "api_admin_fetchcreatetenant", "label": "fetchCreateTenant()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L56"}, {"id": "api_admin_fetchupdatetenant", "label": "fetchUpdateTenant()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L71"}, {"id": "api_admin_fetchdeletetenant", "label": "fetchDeleteTenant()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L89"}, {"id": "api_admin_fetchuserlist", "label": "fetchUserList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L100"}, {"id": "api_admin_fetchuserdetail", "label": "fetchUserDetail()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L119"}, {"id": "api_admin_fetchcreateuser", "label": "fetchCreateUser()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L125"}, {"id": "api_admin_fetchupdateuser", "label": "fetchUpdateUser()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L147"}, {"id": "api_admin_fetchdeleteuser", "label": "fetchDeleteUser()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L170"}, {"id": "api_admin_fetchresetpassword", "label": "fetchResetPassword()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L177"}, {"id": "api_admin_fetchuploaduseravatar", "label": "fetchUploadUserAvatar()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L185"}, {"id": "api_admin_fetchregionlist", "label": "fetchRegionList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L196"}, {"id": "api_admin_fetchactiveregions", "label": "fetchActiveRegions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L214"}, {"id": "api_admin_fetchnotdeletedregions", "label": "fetchNotDeletedRegions()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L231"}, {"id": "api_admin_fetchcategorylist", "label": "fetchCategoryList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L259"}, {"id": "api_admin_fetchcreatecategory", "label": "fetchCreateCategory()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L277"}, {"id": "api_admin_fetchupdatecategory", "label": "fetchUpdateCategory()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L290"}, {"id": "api_admin_fetchdeletecategory", "label": "fetchDeleteCategory()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L307"}, {"id": "api_admin_fetchuploadcategoryimage", "label": "fetchUploadCategoryImage()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L313"}, {"id": "api_admin_fetchunitlist", "label": "fetchUnitList()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/api/admin.ts", "source_location": "L324"}, {"id": "api_admin_fetchcreateunit", "label": "fetchCrLine truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_router_modules_sqxp_ts", "label": "sqxp.ts", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts", "source_location": "L1"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_router_modules_sqxp_ts", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_types_router", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/router/modules/sqxp.ts", "source_location": "L1", "weight": 1.0}], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_quality_inspection_qc_material_usage_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/quality-inspection/qc-material-usage/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_storage_cert_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/storage-cert/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_audit_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol-audit/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_fullmoon_todo_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/fullmoon-todo/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "label": "DonationViewDialog.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L1"}, {"id": "list_donationviewdialog_dash", "label": "dash()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L663"}, {"id": "list_donationviewdialog_formatymd", "label": "formatYmd()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L668"}, {"id": "list_donationviewdialog_formatym", "label": "formatYm()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L674"}, {"id": "list_donationviewdialog_auditordisplay", "label": "auditorDisplay()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L680"}, {"id": "list_donationviewdialog_sampletypename", "label": "sampleTypeName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L686"}, {"id": "list_donationviewdialog_unitname", "label": "unitName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L690"}, {"id": "list_donationviewdialog_sitename", "label": "siteName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L694"}, {"id": "list_donationviewdialog_expressname", "label": "expressName()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L698"}, {"id": "list_donationviewdialog_if", "label": "if()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L703"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "icons_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L491", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_protocolattachmentreadonly_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L492", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_components_business_sample_qc_info_cards_index_vue", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L493", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_research_donation", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L494", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_api_admin", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L499", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": "users_wuzilong_work_00code_zkxw_web_admin_src_utils_download_attachment", "relation": "imports_from", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/research-donation/list/DonationViewDialog.vue", "source_location": "L508", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_views_research_donation_list_donationviewdialog_vue", "target": Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_components_core_layouts_art_work_tab_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L1"}, {"id": "art_work_tab_index_settransition", "label": "setTransition()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L204"}, {"id": "art_work_tab_index_handlewheelscroll", "label": "handleWheelScroll()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L283"}, {"id": "art_work_tab_index_handletouchstart", "label": "handleTouchStart()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L300"}, {"id": "art_work_tab_index_handletouchmove", "label": "handleTouchMove()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L304"}, {"id": "art_work_tab_index_handletouchend", "label": "handleTouchEnd()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L318"}, {"id": "art_work_tab_index_setupeventlisteners", "label": "setupEventListeners()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L322"}, {"id": "art_work_tab_index_cleanupeventlisteners", "label": "cleanupEventListeners()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L331"}, {"id": "art_work_tab_index_clicktab", "label": "clickTab()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L359"}, {"id": "art_work_tab_index_closeworktab", "label": "closeWorktab()", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L370"}], "edges": [{"source": "users_wuzilong_work_00code_zkxw_web_admin_src_components_core_layouts_art_work_tab_index_vue", "target": "art_work_tab_index_settransition", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L204", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_components_core_layouts_art_work_tab_index_vue", "target": "art_work_tab_index_handlewheelscroll", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L283", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_components_core_layouts_art_work_tab_index_vue", "target": "art_work_tab_index_handletouchstart", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L300", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_components_core_layouts_art_work_tab_index_vue", "target": "art_work_tab_index_handletouchmove", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L304", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_components_core_layouts_art_work_tab_index_vue", "target": "art_work_tab_index_handletouchend", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L318", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_components_core_layouts_art_work_tab_index_vue", "target": "art_work_tab_index_setupeventlisteners", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L322", "weight": 1.0}, {"source": "users_wuzilong_work_00code_zkxw_web_admin_src_components_core_layouts_art_work_tab_index_vue", "target": "art_work_tab_index_cleanupeventlisteners", "relation": "contains", "confidence": "EXTRACTED", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/components/core/layouts/art-work-tab/index.vue", "source_location": "L331", "weight": 1.0}, {"source": "users_wuzilong_work_Line truncated
@@ -0,0 +1 @@
{"nodes": [{"id": "users_wuzilong_work_00code_zkxw_web_admin_src_views_cell_storage_protocol_index_vue", "label": "index.vue", "file_type": "code", "source_file": "/Users/wuzilong/work/00code/zkxw/web-admin/src/views/cell-storage/protocol/index.vue", "source_location": "L1"}], "edges": [], "raw_calls": []}
+4
View File
@@ -1,6 +1,10 @@
import './utils/patch-el-select-filterable'
import App from './App.vue'
import { createApp } from 'vue'
import { ElTable } from 'element-plus'
ElTable.props.border = { type: Boolean, default: true }
import { initStore } from './store' // Store
import { initRouter } from './router' // Router
import language from './locales' // 国际化
+2 -2
View File
@@ -85,7 +85,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
name: 'Unit',
path: 'unit',
component: '/system/unit/index',
meta: { title: '物品单位', keepAlive: true, menuCode: 'system:unit:list' }
meta: { title: '物品单位管理', keepAlive: true, menuCode: 'system:unit:list' }
},
{
id: '110',
@@ -183,7 +183,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
name: 'BasicInfoInsuranceCompany',
path: 'insurance-company',
component: '/basic-info/insurance-company/index',
meta: { title: '保险公司及项目价格', keepAlive: true }
meta: { title: '保险公司及险种价格', keepAlive: true }
}
]
},
@@ -22,8 +22,8 @@
<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>
<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>
@@ -209,7 +209,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Collection } 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,
@@ -22,8 +22,8 @@
<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>
<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>
@@ -363,7 +363,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Tickets } from '@element-plus/icons-vue'
import { Plus, Close, Check, View, Tickets, 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 {
@@ -22,8 +22,8 @@
<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>
<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>
@@ -332,7 +332,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Location } from '@element-plus/icons-vue'
import { Plus, Close, Check, View, Location, RefreshLeft, Search } from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import {
@@ -26,8 +26,8 @@
<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>
<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>
@@ -226,7 +226,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, User } from '@element-plus/icons-vue'
import { Plus, Close, Check, View, User, RefreshLeft, Search } from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
fetchCustomerList,
@@ -1,10 +1,10 @@
<!-- 保险公司 — 项目价格配置 -->
<!-- 保险公司 — 险种价格配置 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<header class="page-head">
<h1>保险公司及项目价格</h1>
<p class="page-desc">维护保险公司基础信息、开票信息及项目价格管理。</p>
<h1>保险公司及险种价格</h1>
<p class="page-desc">维护保险公司基础信息、开票信息及险种价格管理。</p>
</header>
<section class="panel">
@@ -80,7 +80,7 @@
<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 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>
@@ -161,7 +161,7 @@
</template>
</el-dialog>
<!-- 项目价格配置弹窗 -->
<!-- 险种价格配置弹窗 -->
<el-dialog
v-model="itemDialogVisible"
:title="itemDialogTitle"
@@ -173,7 +173,7 @@
@closed="onItemDialogClosed"
>
<div class="item-dialog-head">
<span class="muted">当前公司:{{ currentCompanyName }}</span>
<span class="muted">当前保险公司:{{ currentCompanyName }}</span>
</div>
<div class="list-toolbar item-toolbar">
@@ -182,7 +182,7 @@
已选 <span class="sel-num">{{ itemSelected.length }}</span> 项
</div>
<el-button class="btn-main" type="primary" @click="handleAddItem">
<el-icon><Plus /></el-icon>新增项目
<el-icon><Plus /></el-icon>新增险种
</el-button>
</div>
</div>
@@ -198,8 +198,8 @@
>
<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">
<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">
@@ -228,7 +228,7 @@
</div>
</div>
<!-- 新增/编辑项目嵌套弹窗 -->
<!-- 新增/编辑险种嵌套弹窗 -->
<el-dialog
v-model="itemFormVisible"
:title="itemFormTitle"
@@ -241,10 +241,10 @@
>
<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 label="险种名称" prop="projectName">
<el-input v-model="itemForm.projectName" placeholder="请输入险种名称" />
</el-form-item>
<el-form-item label="项目费用(元)" prop="projectFee">
<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">
@@ -344,10 +344,10 @@
}
// ---------------------------------------------------------------------------
// 项目价格配置弹窗
// 险种价格配置弹窗
// ---------------------------------------------------------------------------
const itemDialogVisible = ref(false)
const itemDialogTitle = ref('项目价格配置')
const itemDialogTitle = ref('险种价格配置')
const currentCompanyId = ref('')
const currentCompanyName = ref('')
const itemTable = ref<any[]>([])
@@ -356,7 +356,7 @@
const itemPagination = reactive({ page: 1, pageSize: 10, total: 0 })
const itemFormVisible = ref(false)
const itemFormTitle = ref('新增项目')
const itemFormTitle = ref('新增险种')
const itemIsEdit = ref(false)
const itemSubmitLoading = ref(false)
const itemFormRef = ref()
@@ -368,7 +368,7 @@
remark: ''
})
const itemFormRules = {
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
projectName: [{ required: true, message: '请输入险种名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
@@ -527,7 +527,7 @@
async function handleDelete(row: any) {
try {
await ElMessageBox.confirm(`确定删除「${row.companyName}」?其下属项目价格将一并删除。`, '提示', {
await ElMessageBox.confirm(`确定删除「${row.companyName}」?其下属险种价格将一并删除。`, '提示', {
type: 'warning'
})
} catch {
@@ -582,14 +582,14 @@
}
// ---------------------------------------------------------------------------
// 项目价格配置弹窗
// 险种价格配置弹窗
// ---------------------------------------------------------------------------
function openItemDialog(row: any) {
currentCompanyId.value = row.id
currentCompanyName.value = row.companyName || ''
itemPagination.page = 1
itemSelected.value = []
itemDialogTitle.value = `项目价格配置 — ${row.companyName}`
itemDialogTitle.value = `险种价格配置 — ${row.companyName}`
itemDialogVisible.value = true
loadItems()
}
@@ -627,14 +627,14 @@
function handleAddItem() {
itemIsEdit.value = false
itemFormTitle.value = '新增项目'
itemFormTitle.value = '新增险种'
resetItemForm()
itemFormVisible.value = true
}
function handleEditItem(row: any) {
itemIsEdit.value = true
itemFormTitle.value = '编辑项目'
itemFormTitle.value = '编辑险种'
editingItemId.value = row.id
itemForm.projectName = row.projectName || ''
itemForm.projectFee = row.projectFee ?? 0
@@ -22,8 +22,8 @@
<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>
<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>
@@ -378,7 +378,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Ticket } from '@element-plus/icons-vue'
import { Plus, Close, Check, View, Ticket, 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 {
@@ -4,12 +4,14 @@
<div class="page-container">
<header class="page-head">
<h1>满月待办管理</h1>
<p class="page-desc">集中管理满月待办保险办理情况,支持筛选与批量办理。</p>
<p class="page-desc">
展示全部已生效采集协议;已满月且未办保险的行以红色背景标识,其余为正常底色。
</p>
</header>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">待办列表</h2>
<h2>待办列表</h2>
</div>
<div class="filter-block">
@@ -27,7 +29,7 @@
<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-option label="已办保险" value="1" />
</el-select>
</el-form-item>
</div>
@@ -44,14 +46,34 @@
</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-ghost"
:disabled="selectedRows.length === 0"
@click="handleBatchEdit"
>
<el-icon><EditPen /></el-icon>批量办理保险
</el-button>
</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="fullmoonRowClassName"
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -60,29 +82,34 @@
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">
<el-table-column prop="protocolNo" label="采集协议号" min-width="160" align="center" header-align="center" />
<el-table-column prop="motherName" label="母亲姓名" min-width="100" align="center" header-align="center" />
<el-table-column prop="newbornDeliveryTime" label="新生儿分娩时间" min-width="160" align="center" header-align="center" />
<el-table-column prop="fullmoonDate" label="满月日期" min-width="120" align="center" header-align="center" />
<el-table-column prop="todoStatus" label="待办状态" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag :type="row.todoStatus == 1 ? 'success' : 'warning'" size="small">
{{ row.todoStatus == 1 ? '已办报销' : '未办保险' }}
{{ row.todoStatus == 1 ? '已办保险' : '未办保险' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="insuranceSubscribed" label="是否已订购保险" width="120" align="center" header-align="center">
<el-table-column prop="insuranceSubscribed" label="是否已订购保险" min-width="130" 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">
<el-table-column prop="insuranceCompanyName" label="保险公司" min-width="140" align="center" header-align="center" />
<el-table-column prop="insuranceType" label="保险险种" min-width="120" align="center" header-align="center" />
<el-table-column label="操作" 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="handleView(row)"
><el-icon><View /></el-icon>查看</el-button
>
<el-button class="btn-op-item primary" @click="handleEdit(row)">
<el-icon><EditPen /></el-icon>办理保险
</el-button>
</template>
</el-table-column>
</el-table>
@@ -99,16 +126,31 @@
</div>
</div>
<!-- 查看弹窗(与采集协议单录入等一致) -->
<ProtocolViewDialog
v-model="viewVisible"
:protocol-id="viewProtocolId"
:list-row="viewRow"
show-audit-history
show-storage-cert
show-fullmoon-insurance
/>
<!-- 办理保险弹窗 -->
<el-dialog
v-model="dialogVisible"
title="办理保险"
width="720px"
:title="dialogTitle"
width="960px"
class="dialog-form-layout"
align-center
destroy-on-close
:close-on-click-modal="false"
@closed="handleInsuranceDialogClosed"
>
<div v-if="isBatchMode" class="batch-insurance-tip">
将为已选 <strong>{{ batchEditingRows.length }}</strong> 条记录统一填写保险信息(适用于团体险等场景)。
<div v-if="batchProtocolSummary" class="batch-protocol-nos">{{ batchProtocolSummary }}</div>
</div>
<el-form
ref="formRef"
:model="formData"
@@ -118,7 +160,7 @@
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-2">
<div class="form-grid-3">
<el-form-item label="保险公司" prop="insuranceCompanyId">
<el-select
v-model="formData.insuranceCompanyId"
@@ -136,7 +178,23 @@
</el-select>
</el-form-item>
<el-form-item label="保险险种" prop="insuranceType">
<el-input v-model="formData.insuranceType" placeholder="请输入保险险种" />
<el-select
v-model="formData.insuranceType"
placeholder="请先选择保险公司,再选择险种"
filterable
clearable
:disabled="!formData.insuranceCompanyId"
:loading="insuranceProjectLoading"
style="width: 100%"
@change="handleInsuranceTypeChange"
>
<el-option
v-for="p in insuranceProjectOptions"
:key="p.id || p.projectName"
:label="formatInsuranceProjectLabel(p)"
:value="p.projectName"
/>
</el-select>
</el-form-item>
<el-form-item label="保险单号">
<el-input v-model="formData.insurancePolicyNo" placeholder="请输入保险单号" />
@@ -188,22 +246,81 @@
<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 :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>
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
ref="attachmentUploadRef"
:auto-upload="false"
:show-file-list="false"
:multiple="true"
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
@change="handleAttachmentChange"
>
<div class="upload-card">
<el-icon :size="28" color="#10b981"><Plus /></el-icon>
<span class="upload-card-text">上传附件</span>
</div>
</el-upload>
<div
v-for="(file, index) in formData.attachments"
:key="file.id || index"
class="attachment-item"
:class="{ 'is-uploading': file._uploading }"
>
<div
class="attachment-preview"
@click="
isImageFile(file) && file.fileUrl
? handlePreviewAttachment(file)
: undefined
"
>
<template v-if="isImageFile(file) && file.fileUrl">
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
</template>
<template v-else-if="isImageFile(file) && !file.fileUrl">
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
</template>
<template v-else-if="isVideoFile(file)">
<div class="attachment-preview__icon attachment-preview__icon--video">
<el-icon :size="28"><VideoPlay /></el-icon>
</div>
</template>
<template v-else>
<div class="attachment-preview__icon attachment-preview__icon--doc">
<el-icon :size="28"><Document /></el-icon>
</div>
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{
file.fileName
}}</span>
<div class="attachment-actions">
<el-icon
class="action-btn"
title="下载"
@click="handleDownloadAttachment(file)"
><Download
/></el-icon>
<el-icon
class="action-btn action-btn--danger"
title="删除"
@click="removeAttachment(index)"
><Delete
/></el-icon>
</div>
</div>
</div>
</div>
<span class="upload-hint"
>支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB</span
>
</div>
</el-form-item>
<el-form-item label="办理备注" class="form-col-span-2">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入办理备注" />
@@ -219,18 +336,48 @@
</el-button>
</template>
</el-dialog>
<!-- 附件预览弹窗 -->
<el-dialog
v-model="previewVisible"
width="800px"
align-center
destroy-on-close
:show-close="true"
style="backdrop-filter: none"
>
<img v-if="previewUrl" :src="previewUrl" alt="预览" style="width: 100%" />
</el-dialog>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, computed, nextTick, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Close, Check, Search, RefreshLeft } from '@element-plus/icons-vue'
import {
Close,
Check,
Search,
RefreshLeft,
View,
EditPen,
Plus,
Loading,
VideoPlay,
Document,
Download,
Delete
} from '@element-plus/icons-vue'
import ProtocolViewDialog from '../protocol/ProtocolViewDialog.vue'
import type { CollectionProtocolRow } from '@/api/collection-protocol'
import { fetchUploadAttachment } from '@/api/business'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import {
fetchFullmoonTodoList,
fetchUpdateFullmoonTodoInsurance,
fetchInsuranceCompanyList
fetchInsuranceCompanyList,
fetchInsuranceProjectPrices
} from '@/api/admin'
defineOptions({ name: 'CellStorageFullmoonTodo' })
@@ -240,9 +387,57 @@
// ---------------------------------------------------------------------------
const searchForm = reactive({ keyword: '', todoStatus: '' })
const tableData = ref<any[]>([])
const tableRef = ref()
const selectedRows = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows
}
function clearTableSelection() {
tableRef.value?.clearSelection?.()
selectedRows.value = []
}
// ---------------------------------------------------------------------------
// 查看弹窗
// ---------------------------------------------------------------------------
const viewVisible = ref(false)
const viewProtocolId = ref<string | null>(null)
const viewRow = ref<CollectionProtocolRow | null>(null)
function handleView(row: any) {
const pid = String(row.protocolId || row.id || '')
if (!pid) {
ElMessage.warning('缺少关联协议单,无法查看')
return
}
viewProtocolId.value = pid
viewRow.value = row as CollectionProtocolRow
viewVisible.value = true
}
/** 已满月且待办状态非「已办保险」时标红 */
function isInsuranceTodoPendingRow(row: Record<string, unknown>) {
if (row.isFullmoonPassed === true) {
return String(row.todoStatus ?? '0') !== '1'
}
const fullmoonDate = String(row.fullmoonDate || '').trim()
if (!fullmoonDate) return false
const fullmoon = new Date(fullmoonDate.replace(/-/g, '/'))
const today = new Date()
today.setHours(0, 0, 0, 0)
fullmoon.setHours(0, 0, 0, 0)
if (Number.isNaN(fullmoon.getTime()) || fullmoon > today) return false
return String(row.todoStatus ?? '0') !== '1'
}
function fullmoonRowClassName({ row }: { row: Record<string, unknown> }) {
return isInsuranceTodoPendingRow(row) ? 'row-insurance-pending' : ''
}
// ---------------------------------------------------------------------------
// 保险公司下拉
// ---------------------------------------------------------------------------
@@ -257,6 +452,59 @@
}
}
// ---------------------------------------------------------------------------
// 险种价格下拉(按保险公司联动)
// ---------------------------------------------------------------------------
type InsuranceProjectOption = {
id: string
projectName: string
projectFee: number | null
status?: string
}
const insuranceProjectOptions = ref<InsuranceProjectOption[]>([])
const insuranceProjectLoading = ref(false)
function formatInsuranceProjectLabel(p: InsuranceProjectOption) {
if (p.projectFee != null && !Number.isNaN(Number(p.projectFee))) {
return `${p.projectName}(¥${p.projectFee})`
}
return p.projectName
}
async function loadInsuranceProjects(companyId: string, keepType?: string) {
if (!companyId) {
insuranceProjectOptions.value = []
return
}
insuranceProjectLoading.value = true
try {
const res = await fetchInsuranceProjectPrices(companyId)
const list = Array.isArray(res) ? res : (res as any)?.list || []
let options: InsuranceProjectOption[] = list
.filter((p: any) => String(p.status ?? '1') === '1')
.map((p: any) => ({
id: p.id || '',
projectName: p.projectName || '',
projectFee: p.projectFee != null ? Number(p.projectFee) : null,
status: p.status
}))
.filter((p: InsuranceProjectOption) => p.projectName)
const typeName = (keepType || '').trim()
if (typeName && !options.some((p) => p.projectName === typeName)) {
options = [{ id: '', projectName: typeName, projectFee: null }, ...options]
}
insuranceProjectOptions.value = options
} catch {
const typeName = (keepType || '').trim()
insuranceProjectOptions.value = typeName
? [{ id: '', projectName: typeName, projectFee: null }]
: []
} finally {
insuranceProjectLoading.value = false
}
}
// ---------------------------------------------------------------------------
// 主表数据加载
// ---------------------------------------------------------------------------
@@ -295,6 +543,21 @@
const submitLoading = ref(false)
const formRef = ref()
const editingRow = ref<any>(null)
const isBatchMode = ref(false)
const batchEditingRows = ref<any[]>([])
const dialogTitle = computed(() => (isBatchMode.value ? '批量办理保险' : '办理保险'))
const batchProtocolSummary = computed(() => {
const nos = batchEditingRows.value.map((r) => r.protocolNo).filter(Boolean)
if (!nos.length) return ''
if (nos.length <= 5) return `协议号:${nos.join('、')}`
return `协议号:${nos.slice(0, 5).join('、')} 等 ${nos.length} 条`
})
function resolveProtocolId(row: any) {
return String(row?.protocolId || row?.id || '').trim()
}
const formData = reactive({
insuranceCompanyId: '' as string,
@@ -313,13 +576,14 @@
const formRules = {
insuranceCompanyId: [{ required: true, message: '请选择保险公司', trigger: 'change' }],
insuranceType: [{ required: true, message: '请输入保险险种', trigger: 'blur' }]
insuranceType: [{ required: true, message: '请选择保险险种', trigger: 'change' }]
}
function resetForm() {
function resetFormFields() {
formData.insuranceCompanyId = ''
formData.insuranceCompanyName = ''
formData.insuranceType = ''
insuranceProjectOptions.value = []
formData.insurancePolicyNo = ''
formData.policyEffectiveDate = ''
formData.policyExpiryDate = ''
@@ -329,24 +593,88 @@
formData.remark = ''
formData.todoStatus = 0
formData.attachments = []
editingRow.value = null
}
function handleEdit(row: any) {
function resetDialogContext() {
resetFormFields()
editingRow.value = null
isBatchMode.value = false
batchEditingRows.value = []
}
function handleInsuranceDialogClosed() {
resetDialogContext()
}
function parseInsuranceAttachmentsFromUrls(urls: string) {
return String(urls || '')
.split(',')
.map((u) => u.trim())
.filter(Boolean)
.map((url, index) => {
const rawName = url.split('/').pop() || ''
const fileName = decodeURIComponent(rawName) || `附件${index + 1}`
return { id: `existing_${index}`, fileName, fileUrl: url, fileType: '' }
})
}
function buildInsuranceAttachmentsValue() {
return formData.attachments
.filter((f: any) => f.fileUrl && !f._uploading)
.map((f: any) => f.fileUrl)
.join(',')
}
function buildInsurancePayload() {
return {
insuranceCompanyId: formData.insuranceCompanyId || undefined,
insuranceCompanyName: formData.insuranceCompanyName || undefined,
insuranceType: formData.insuranceType?.trim() || undefined,
insurancePolicyNo: formData.insurancePolicyNo?.trim() || undefined,
insuranceEffectiveDate: formData.policyEffectiveDate || undefined,
insuranceExpiryDate: formData.policyExpiryDate || undefined,
insuranceSubscribed: String(formData.insuranceSubscribed ?? 0),
insuranceFee: formData.insuranceFee || undefined,
handleDate: formData.processDate || undefined,
handleRemark: formData.remark?.trim() || undefined,
todoStatus: String(formData.todoStatus ?? 0),
insuranceAttachments: buildInsuranceAttachmentsValue()
}
}
function handleBatchEdit() {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先选择待办记录')
return
}
resetFormFields()
isBatchMode.value = true
batchEditingRows.value = [...selectedRows.value]
editingRow.value = null
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
async function handleEdit(row: any) {
resetDialogContext()
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.policyEffectiveDate =
row.insuranceEffectiveDate || row.policyEffectiveDate || ''
formData.policyExpiryDate = row.insuranceExpiryDate || row.policyExpiryDate || ''
formData.insuranceSubscribed = Number(row.insuranceSubscribed ?? 0)
formData.insuranceFee = row.insuranceFee ?? 0
formData.processDate = row.processDate || ''
formData.remark = row.remark || ''
formData.todoStatus = row.todoStatus ?? 0
formData.processDate = row.handleDate || row.processDate || ''
formData.remark = row.handleRemark || row.remark || ''
formData.todoStatus = Number(row.todoStatus ?? 0)
formData.attachments = parseInsuranceAttachmentsFromUrls(row.insuranceAttachments || '')
if (formData.insuranceCompanyId) {
await loadInsuranceProjects(formData.insuranceCompanyId, formData.insuranceType)
}
dialogVisible.value = true
formRef.value?.clearValidate?.()
}
@@ -354,37 +682,171 @@
function handleInsuranceCompanyChange(val: string) {
const found = insuranceCompanyOptions.value.find((c) => c.id === val)
formData.insuranceCompanyName = found ? found.companyName : ''
formData.insuranceType = ''
formData.insuranceFee = 0
loadInsuranceProjects(val)
}
function handleInsuranceTypeChange(val: string) {
const found = insuranceProjectOptions.value.find((p) => p.projectName === val)
if (found?.projectFee != null && !Number.isNaN(Number(found.projectFee))) {
formData.insuranceFee = Number(found.projectFee)
}
}
async function handleSubmit() {
await formRef.value?.validate?.()
if (!editingRow.value?.protocolId) return
if (formData.attachments.some((f: any) => f._uploading)) {
ElMessage.warning('附件上传中,请稍候')
return
}
const payload = buildInsurancePayload()
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
if (isBatchMode.value) {
const rows = batchEditingRows.value
let successCount = 0
const failedNos: string[] = []
for (const row of rows) {
const protocolId = resolveProtocolId(row)
if (!protocolId) {
failedNos.push(row.protocolNo || '未知协议')
continue
}
try {
await fetchUpdateFullmoonTodoInsurance(protocolId, payload)
successCount++
} catch {
failedNos.push(row.protocolNo || protocolId)
}
}
if (successCount === 0) {
ElMessage.error('批量办理失败,请重试')
return
}
ElMessage.success(
failedNos.length
? `已成功办理 ${successCount} 条,${failedNos.length} 条失败`
: `已成功为 ${successCount} 条记录办理保险`
)
dialogVisible.value = false
clearTableSelection()
await loadData()
return
}
await fetchUpdateFullmoonTodoInsurance(editingRow.value.protocolId, payload)
const protocolId = resolveProtocolId(editingRow.value)
if (!protocolId) {
ElMessage.warning('缺少关联协议,无法保存')
return
}
await fetchUpdateFullmoonTodoInsurance(protocolId, payload)
ElMessage.success('保存成功')
dialogVisible.value = false
await loadData()
} catch {
/* 校验或接口错误已由全局处理 */
/* 单条校验或接口错误已由全局处理 */
} finally {
submitLoading.value = false
}
}
// ---------------------------------------------------------------------------
// 附件上传(卡片化布局)
// ---------------------------------------------------------------------------
const attachmentUploadRef = ref()
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
return `${minioBaseUrl}/${suffix}`
}
function isImageFile(file: any) {
const url = (file.fileUrl || '').toLowerCase()
const name = (file.fileName || '').toLowerCase()
return (
/\.(jpe?g|png|gif|bmp|webp|svg|ico|tiff?)(\?|$)/.test(url) ||
/\.(jpe?g|png|gif|bmp|webp|svg|ico|tiff?)$/.test(name) ||
(file.fileType || '').startsWith('image/')
)
}
function isVideoFile(file: any) {
const url = (file.fileUrl || '').toLowerCase()
const name = (file.fileName || '').toLowerCase()
return (
/\.(mp4|webm|ogg|mov|avi)(\?|$)/.test(url) ||
/\.(mp4|webm|ogg|mov|avi)$/.test(name) ||
(file.fileType || '').startsWith('video/')
)
}
async function handleAttachmentChange(uploadFile: any) {
const file = uploadFile.raw || uploadFile
if (!file) return
const maxSizeMb = 20
if (file.size / 1024 / 1024 > maxSizeMb) {
ElMessage.error(`文件大小不能超过 ${maxSizeMb}MB`)
attachmentUploadRef.value?.clearFiles()
return
}
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
formData.attachments.push({
id: tempId,
fileName: file.name,
fileUrl: '',
fileSize: file.size,
fileSizeDisplay: `${(file.size / 1024).toFixed(1)} KB`,
fileType: file.type || '',
_uploading: true
})
const idx = formData.attachments.length - 1
try {
const fd = new FormData()
fd.append('folder', 'fullmoon-todo')
fd.append('file', file)
const res = await fetchUploadAttachment(fd)
formData.attachments[idx] = {
...formData.attachments[idx],
id: res.id || tempId,
fileName: file.name || res.fileName,
fileUrl: res.fileUrl,
fileSize: res.fileSize || file.size,
fileSizeDisplay: res.fileSizeDisplay || `${(file.size / 1024).toFixed(1)} KB`,
fileType: res.fileType || file.type || '',
_uploading: false
}
} catch {
ElMessage.error('文件上传失败')
formData.attachments.splice(idx, 1)
} finally {
nextTick(() => attachmentUploadRef.value?.clearFiles())
}
}
function removeAttachment(index: number) {
formData.attachments.splice(index, 1)
}
/* 附件预览 */
const previewVisible = ref(false)
const previewUrl = ref('')
function handlePreviewAttachment(file: any) {
if (file._uploading) return
previewUrl.value = fullFileUrl(file.fileUrl)
previewVisible.value = true
}
function handleDownloadAttachment(file: any) {
downloadAttachmentFile(file, { fullFileUrl })
}
onMounted(() => {
loadData()
loadInsuranceCompanies()
@@ -392,9 +854,164 @@
</script>
<style scoped lang="scss">
.batch-insurance-tip {
margin-bottom: 12px;
padding: 10px 12px;
background: #f0fdf4;
border: 1px solid #a7f3d0;
border-radius: 8px;
font-size: 13px;
color: #334155;
line-height: 1.5;
strong {
color: #059669;
}
}
.batch-protocol-nos {
margin-top: 6px;
font-size: 12px;
color: #64748b;
}
.biz-dialog-form {
:deep(.el-form-item) {
margin-bottom: 0;
}
}
/* 附件卡片化布局 */
.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;
&: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;
margin-top: 8px;
line-height: 1;
}
.attachment-item {
width: 140px;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
overflow: hidden;
&.is-uploading {
opacity: 0.6;
pointer-events: none;
}
&:hover {
border-color: #10b981;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1);
}
}
.attachment-preview {
width: 100%;
height: 96px;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
overflow: hidden;
cursor: pointer;
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;
&--video {
background: #ede9fe;
color: #7c3aed;
}
&--doc {
background: #e0f2fe;
color: #0284c7;
}
}
.attachment-footer {
display: flex;
flex-direction: column;
padding: 6px 8px;
}
.attachment-name {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: 11px;
color: #334155;
line-height: 1.4;
word-break: break-all;
}
.attachment-actions {
display: flex;
justify-content: center;
gap: 8px;
margin-top: 4px;
}
.action-btn {
width: 22px;
height: 22px;
border-radius: 4px;
background: #f1f5f9;
color: #64748b;
cursor: pointer;
&:hover {
background: #10b981;
color: #fff;
}
&--danger:hover {
background: #ef4444;
color: #fff;
}
}
.list-table :deep(tr.row-insurance-pending > td.el-table__cell) {
background-color: #fee2e2 !important;
color: #991b1b;
}
.list-table :deep(tr.row-insurance-pending:hover > td.el-table__cell) {
background-color: #fecaca !important;
}
</style>
@@ -39,8 +39,8 @@
<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>
<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>
@@ -53,6 +53,7 @@
v-loading="loading"
stripe
row-key="id"
@row-dblclick="handleView"
>
<el-table-column
type="index"
@@ -126,6 +127,13 @@
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" @click="handleView(row)">
<el-icon><View /></el-icon>查看
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
@@ -139,12 +147,23 @@
/>
</div>
</div>
<!-- 查看弹窗 -->
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewRow?.materialName || '变动记录详情'"
icon="Document"
:meta="viewRow?.createTime || ''"
:sections="viewSections"
/>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { RefreshLeft, Search, View } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { fetchMaterialStockLogList } from '@/api/material'
import type { MaterialStockLog } from '@/api/material'
@@ -156,6 +175,41 @@
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const viewVisible = ref(false)
const viewRow = ref<MaterialStockLog | 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: row.changeType === 'rk' ? '入库' : '出库',
tagType: row.changeType === 'rk' ? 'success' : 'warning'
},
{ label: '变动数量', value: `${row.changeType === 'rk' ? '+' : ''}${row.changeQty}` },
{ 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.createBy },
{ label: '操作时间', value: row.createTime },
{ label: '备注', value: row.remark || '—', span: true }
]
}
]
})
function handleView(row: MaterialStockLog) {
viewRow.value = row
viewVisible.value = true
}
async function loadData() {
loading.value = true
try {
@@ -38,8 +38,8 @@
<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>
<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>
@@ -125,15 +125,15 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
<el-table-column label="操作" width="400" 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="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-button>
<el-button class="btn-op-item" @click="handleDelete(row)">删除</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>
@@ -354,7 +354,7 @@
<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 { 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 } from '@/api/admin'
import {
@@ -403,6 +403,7 @@
:protocol-id="viewProtocolId"
:list-row="viewRow"
show-audit-history
show-storage-cert
/>
</section>
</div>
@@ -854,11 +855,11 @@
}
.list-table :deep(tr.row-due-soon > td.el-table__cell) {
background-color: #fca5a5 !important;
color: #7f1d1d;
background-color: #fee2e2 !important;
color: #991b1b;
}
.list-table :deep(tr.row-due-soon:hover > td.el-table__cell) {
background-color: #f87171 !important;
background-color: #fecaca !important;
}
</style>
@@ -289,6 +289,7 @@
:protocol-id="viewProtocolId"
:list-row="viewRow"
show-audit-history
show-storage-cert
/>
</section>
</div>
@@ -11,7 +11,7 @@
@update:model-value="emit('update:modelValue', $event)"
@closed="handleClosed"
>
<div v-loading="loading" class="protocol-view-body">
<div v-loading="detailLoading" class="protocol-view-body">
<div class="detail-tabs">
<div
v-for="tab in formTabs"
@@ -28,41 +28,77 @@
<!-- KPI 摘要条 -->
<div class="kpi-strip">
<div class="kpi-card">
<div class="kpi-label">协议编号</div>
<div class="kpi-value">{{ dash(protocol?.protocolNo) }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">协议状态</div>
<div class="kpi-value">
<el-tag :type="protocolStatusTagType(String(protocol?.protocolStatus || ''))" size="small">
{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}
</el-tag>
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><Document /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">协议编号</div>
<div class="kpi-value kpi-value--emphasis">{{ dash(protocol?.protocolNo) }}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">母亲姓名</div>
<div class="kpi-value">{{ dash(partyCustomer?.motherName) }}</div>
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">协议状态</div>
<div class="kpi-value">
<el-tag :type="protocolStatusTagType(String(protocol?.protocolStatus || ''))" size="small">
{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}
</el-tag>
</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">登记日期</div>
<div class="kpi-value">{{ formatYmd(protocol?.registerDate) }}</div>
<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(partyCustomer?.motherName) }}</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">{{ formatYmd(protocol?.registerDate) }}</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-grid data-grid--4">
<div class="data-item"><div class="data-label">协议号</div><div class="data-value">{{ dash(protocol?.protocolNo) }}</div></div>
<div class="data-item"><div class="data-label">协议状态</div><div class="data-value"><el-tag :type="protocolStatusTagType(String(protocol?.protocolStatus || ''))" size="small">{{ protocolStatusLabel(String(protocol?.protocolStatus || '')) }}</el-tag></div></div>
<div class="data-item"><div class="data-label">登记日期</div><div class="data-value">{{ formatYmd(protocol?.registerDate) }}</div></div>
<div class="data-item"><div class="data-label">采集专员</div><div class="data-value">{{ collectorDisplay }}</div></div>
<div class="data-item"><div class="data-label">创建时间</div><div class="data-value">{{ dash(protocol?.createTime) }}</div></div>
<template v-if="showStorageCert">
<div class="data-item">
<div class="data-label">制作状态</div>
<div class="data-value">
<el-tag
:type="storageCertMakeStatusTagType(String(storageCertInfo?.makeStatus || '0'))"
size="small"
>
{{ storageCertMakeStatusLabel(String(storageCertInfo?.makeStatus || '0')) }}
</el-tag>
</div>
</div>
<div class="data-item">
<div class="data-label">制作日期</div>
<div class="data-value">{{ formatYmd(storageCertInfo?.makeDate) }}</div>
</div>
</template>
</div>
<!-- 协议状态变更记录 -->
<div v-if="hasAnyStatusChange" class="status-change-section">
<!-- 协议状态变更记录(非储存证查看:仍在基本信息内) -->
<div v-if="!showStorageCert && hasAnyStatusChange" class="status-change-section">
<div class="status-change-title">状态变更记录</div>
<div class="data-grid data-grid--2">
<div class="data-grid data-grid--4">
<template v-if="protocol?.effectiveTime">
<div class="data-item"><div class="data-label">协议生效时间</div><div class="data-value">{{ dash(protocol.effectiveTime) }}</div></div>
<div class="data-item"><div class="data-label">协议生效描述</div><div class="data-value data-value--muted">{{ dash(protocol.effectiveDesc) }}</div></div>
@@ -81,7 +117,16 @@
</template>
</div>
</div>
<!-- 附件行 -->
<!-- 储存证附件(储存证查看:在协议附件上方) -->
<div v-if="showStorageCert" class="attachment-row">
<div class="data-label">储存证附件</div>
<ProtocolAttachmentReadonly
:files="storageCertAttachments"
@preview="openPreview"
@download="downloadFile"
/>
</div>
<!-- 协议附件 -->
<div class="attachment-row" v-if="protocolAttachments.length">
<div class="data-label">协议附件</div>
<ProtocolAttachmentReadonly :files="protocolAttachments" @preview="openPreview" @download="downloadFile" />
@@ -92,6 +137,30 @@
</div>
</div>
<!-- 状态变更记录(储存证查看:独立区域) -->
<div v-if="showStorageCert" id="view-section-status-change" class="section-card section-card--white">
<div class="section-title"><h3>状态变更记录</h3></div>
<el-empty v-if="!hasAnyStatusChange" description="暂无状态变更记录" :image-size="64" />
<div v-else class="data-grid data-grid--4">
<template v-if="protocol?.effectiveTime">
<div class="data-item"><div class="data-label">协议生效时间</div><div class="data-value">{{ dash(protocol.effectiveTime) }}</div></div>
<div class="data-item"><div class="data-label">协议生效描述</div><div class="data-value data-value--muted">{{ dash(protocol.effectiveDesc) }}</div></div>
</template>
<template v-if="protocol?.expireTime">
<div class="data-item"><div class="data-label">协议过期时间</div><div class="data-value">{{ dash(protocol.expireTime) }}</div></div>
<div class="data-item"><div class="data-label">协议过期描述</div><div class="data-value data-value--muted">{{ dash(protocol.expireDesc) }}</div></div>
</template>
<template v-if="protocol?.voidTime">
<div class="data-item"><div class="data-label">协议作废时间</div><div class="data-value">{{ dash(protocol.voidTime) }}</div></div>
<div class="data-item"><div class="data-label">协议作废描述</div><div class="data-value data-value--muted">{{ dash(protocol.voidDesc) }}</div></div>
</template>
<template v-if="protocol?.toResearchTime">
<div class="data-item"><div class="data-label">协议转科研时间</div><div class="data-value">{{ dash(protocol.toResearchTime) }}</div></div>
<div class="data-item"><div class="data-label">协议转科研描述</div><div class="data-value data-value--muted">{{ dash(protocol.toResearchDesc) }}</div></div>
</template>
</div>
</div>
<!-- 客户信息 -->
<div id="view-section-party" class="section-card section-card--green">
<div class="section-title"><h3>客户信息</h3></div>
@@ -99,7 +168,7 @@
<!-- 母亲信息 -->
<div class="client-sub-card">
<div class="sub-card-header"><span class="sub-card-icon">🤱</span><h4>母亲信息</h4></div>
<div class="data-grid data-grid--2">
<div class="data-grid data-grid--4">
<div class="data-item"><div class="data-label">关联客户</div><div class="data-value">{{ partyCustomer?.customerId ? dash(partyCustomer?.motherName) : '—' }}</div></div>
<div class="data-item"><div class="data-label">母亲姓名</div><div class="data-value">{{ dash(partyCustomer?.motherName) }}</div></div>
<div class="data-item"><div class="data-label">母亲联系电话</div><div class="data-value">{{ dash(partyCustomer?.motherPhone) }}</div></div>
@@ -112,7 +181,7 @@
<!-- 父亲信息 -->
<div class="client-sub-card">
<div class="sub-card-header"><span class="sub-card-icon">👨</span><h4>父亲信息</h4></div>
<div class="data-grid data-grid--2">
<div class="data-grid data-grid--4">
<div class="data-item"><div class="data-label">父亲姓名</div><div class="data-value">{{ dash(partyCustomer?.fatherName) }}</div></div>
<div class="data-item"><div class="data-label">父亲电话</div><div class="data-value">{{ dash(partyCustomer?.fatherPhone) }}</div></div>
<div class="data-item data-item--full"><div class="data-label">父亲身份证号</div><div class="data-value">{{ dash(partyCustomer?.fatherIdCard) }}</div></div>
@@ -121,7 +190,7 @@
<!-- 新生儿信息 -->
<div class="client-sub-card">
<div class="sub-card-header"><span class="sub-card-icon">👶</span><h4>新生儿信息</h4></div>
<div class="data-grid data-grid--2">
<div class="data-grid data-grid--4">
<div class="data-item"><div class="data-label">新生儿姓名</div><div class="data-value">{{ dash(partyCustomer?.newbornName) }}</div></div>
<div class="data-item"><div class="data-label">新生儿身份证</div><div class="data-value">{{ dash(partyCustomer?.newbornIdCard) }}</div></div>
<div class="data-item"><div class="data-label">新生儿分娩时间</div><div class="data-value">{{ dash(partyCustomer?.newbornDeliveryTime) }}</div></div>
@@ -136,10 +205,67 @@
</div>
</div>
<!-- 新生儿保险信息(满月待办查看:客户信息下方独立区域) -->
<div
v-if="showFullmoonInsurance"
id="view-section-fullmoon-insurance"
class="section-card section-card--white"
>
<div class="section-title"><h3>新生儿保险信息</h3></div>
<div class="data-grid data-grid--4">
<div class="data-item"><div class="data-label">满月日期</div><div class="data-value">{{ dash(fullmoonInsuranceInfo?.fullmoonDate) }}</div></div>
<div class="data-item">
<div class="data-label">待办状态</div>
<div class="data-value">
<el-tag
:type="fullmoonTodoStatusTagType(String(fullmoonInsuranceInfo?.todoStatus || '0'))"
size="small"
>
{{ fullmoonTodoStatusLabel(String(fullmoonInsuranceInfo?.todoStatus || '0')) }}
</el-tag>
</div>
</div>
<div class="data-item">
<div class="data-label">是否已订购保险</div>
<div class="data-value">
<el-tag
:type="insuranceSubscribedTagType(String(fullmoonInsuranceInfo?.insuranceSubscribed || '0'))"
size="small"
>
{{ insuranceSubscribedLabel(String(fullmoonInsuranceInfo?.insuranceSubscribed || '0')) }}
</el-tag>
</div>
</div>
<div class="data-item"><div class="data-label">保险公司</div><div class="data-value">{{ dash(fullmoonInsuranceInfo?.insuranceCompanyName) }}</div></div>
<div class="data-item"><div class="data-label">保险险种</div><div class="data-value">{{ dash(fullmoonInsuranceInfo?.insuranceType) }}</div></div>
<div class="data-item"><div class="data-label">保险单号</div><div class="data-value">{{ dash(fullmoonInsuranceInfo?.insurancePolicyNo) }}</div></div>
<div class="data-item"><div class="data-label">保单费用(元)</div><div class="data-value">{{ formatMoney(fullmoonInsuranceInfo?.insuranceFee) }}</div></div>
<div class="data-item"><div class="data-label">保险单生效时间</div><div class="data-value">{{ formatYmd(fullmoonInsuranceInfo?.insuranceEffectiveDate) }}</div></div>
<div class="data-item"><div class="data-label">保险单失效时间</div><div class="data-value">{{ formatYmd(fullmoonInsuranceInfo?.insuranceExpiryDate) }}</div></div>
<div class="data-item"><div class="data-label">办理日期</div><div class="data-value">{{ formatYmd(fullmoonInsuranceInfo?.handleDate) }}</div></div>
</div>
<div v-if="fullmoonInsuranceInfo?.handleRemark" class="remark-row">
<div class="data-item data-item--full">
<div class="data-label">办理备注</div>
<div class="data-value data-value--muted">{{ dash(fullmoonInsuranceInfo?.handleRemark) }}</div>
</div>
</div>
<div class="attachment-row">
<div class="data-label">保险单附件</div>
<ProtocolAttachmentReadonly
v-if="fullmoonInsuranceAttachments.length"
:files="fullmoonInsuranceAttachments"
@preview="openPreview"
@download="downloadFile"
/>
<div v-else class="data-value data-value--muted">—</div>
</div>
</div>
<!-- 采集信息 -->
<div id="view-section-collection" class="section-card section-card--white">
<div class="section-title"><h3>采集信息</h3></div>
<div class="data-grid data-grid--3">
<div class="data-grid data-grid--4">
<div class="data-item"><div class="data-label">采集地区</div><div class="data-value">{{ regionDisplay }}</div></div>
<div class="data-item"><div class="data-label">采集医院</div><div class="data-value">{{ hospitalDisplay }}</div></div>
<div class="data-item"><div class="data-label">采集医护</div><div class="data-value">{{ dash(collectionInfo?.medicalStaff) }}</div></div>
@@ -149,8 +275,12 @@
</div>
</div>
<!-- 样本及运输 -->
<div id="view-section-sample" class="section-card section-card--white">
<!-- 样本及运输(字典异步补齐,不阻塞整弹窗) -->
<div
id="view-section-sample"
v-loading="lookupLoading"
class="section-card section-card--white"
>
<div class="section-title"><h3>样本及运输信息</h3></div>
<div v-if="samples.length" class="sample-cards">
<div
@@ -170,7 +300,7 @@
<div class="sample-card__body">
<div class="sample-card__section">
<div class="sample-card__section-label">样本信息</div>
<div class="data-grid data-grid--3">
<div class="data-grid data-grid--4">
<div class="data-item">
<div class="data-label">样本编码</div>
<div class="data-value data-value--code">{{ dash(row.sampleCode) }}</div>
@@ -195,7 +325,7 @@
</div>
<div v-if="row.transport?.transportType" class="sample-card__section sample-card__section--transport">
<div class="sample-card__section-label">运输信息</div>
<div class="data-grid data-grid--3">
<div class="data-grid data-grid--4">
<div class="data-item">
<div class="data-label">运输类型</div>
<div class="data-value">{{ transportTypeLabel(String(row.transport?.transportType || '')) }}</div>
@@ -217,7 +347,7 @@
<div class="sample-card__section sample-card__section--reception">
<div class="sample-card__section-label">样本接收信息</div>
<template v-if="row.sampleReception">
<div class="data-grid data-grid--3">
<div class="data-grid data-grid--4">
<div class="data-item">
<div class="data-label">接收时间</div>
<div class="data-value">{{ dash(row.sampleReception.receptionTime) }}</div>
@@ -236,6 +366,10 @@
}}
</div>
</div>
<div class="data-item">
<div class="data-label">净重(g)</div>
<div class="data-value">{{ formatMetric(row.sampleReception.netWeightG) }}</div>
</div>
<div class="data-item">
<div class="data-label">结论</div>
<div class="data-value">
@@ -642,7 +776,7 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import { Close } from '@element-plus/icons-vue'
import { Calendar, CircleCheck, Close, Document, User } from '@element-plus/icons-vue'
import ProtocolAttachmentReadonly from './ProtocolAttachmentReadonly.vue'
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
import {
@@ -684,8 +818,12 @@
listRow?: CollectionProtocolRow | null
/** 展示审核记录 Tab(采集协议单审核页) */
showAuditHistory?: boolean
/** 储存证管理查看:展示储存证附件,状态变更记录独立区域 */
showStorageCert?: boolean
/** 满月待办查看:客户信息下方独立展示新生儿保险信息 */
showFullmoonInsurance?: boolean
}>(),
{ showAuditHistory: false }
{ showAuditHistory: false, showStorageCert: false, showFullmoonInsurance: false }
)
const emit = defineEmits<{
@@ -702,13 +840,18 @@
const formTabs = computed(() => {
const tabs: Array<{ key: string; label: string }> = [...baseFormTabs]
if (props.showFullmoonInsurance) {
const partyIdx = tabs.findIndex((t) => t.key === 'party')
tabs.splice(partyIdx + 1, 0, { key: 'fullmoon-insurance', label: '新生儿保险' })
}
if (props.showAuditHistory) {
tabs.push({ key: 'audit', label: '审核记录' })
}
return tabs
})
const loading = ref(false)
const detailLoading = ref(false)
const lookupLoading = ref(false)
const auditLoading = ref(false)
const bundle = ref<Record<string, any> | null>(null)
const auditRecords = ref<Array<Record<string, unknown>>>([])
@@ -760,6 +903,98 @@
return Array.isArray(list) ? list : []
})
const storageCertInfo = computed(() => {
const sc = bundle.value?.storageCert as Record<string, any> | undefined | null
if (sc) return sc
if (!props.showStorageCert || !props.listRow) return null
return {
makeStatus: props.listRow.makeStatus ?? '0',
certImageUrls: props.listRow.certImageUrls ?? '',
makeDate: props.listRow.makeDate ?? '',
remark: props.listRow.remark ?? ''
}
})
const storageCertAttachments = computed(() => {
const urls = String(storageCertInfo.value?.certImageUrls || '').trim()
if (!urls) return []
return urls
.split(',')
.map((u) => u.trim())
.filter(Boolean)
.map((url, index) => {
const rawName = url.split('/').pop() || ''
const fileName = decodeURIComponent(rawName) || `附件${index + 1}`
return { id: `cert_${index}`, fileName, fileUrl: url, fileType: '' }
})
})
const fullmoonInsuranceInfo = computed(() => {
const fm = bundle.value?.fullmoonTodo as Record<string, any> | undefined
if (fm) return fm
if (!props.showFullmoonInsurance || !props.listRow) return null
const row = props.listRow as Record<string, any>
return {
fullmoonDate: row.fullmoonDate ?? '',
todoStatus: row.todoStatus ?? '0',
insuranceSubscribed: row.insuranceSubscribed ?? '0',
insuranceCompanyName: row.insuranceCompanyName ?? '',
insuranceType: row.insuranceType ?? '',
insurancePolicyNo: row.insurancePolicyNo ?? '',
insuranceEffectiveDate: row.insuranceEffectiveDate ?? row.policyEffectiveDate ?? '',
insuranceExpiryDate: row.insuranceExpiryDate ?? row.policyExpiryDate ?? '',
insuranceFee: row.insuranceFee,
handleDate: row.handleDate ?? row.processDate ?? '',
handleRemark: row.handleRemark ?? row.remark ?? '',
insuranceAttachments: row.insuranceAttachments ?? ''
}
})
const fullmoonInsuranceAttachments = computed(() => {
const urls = String(fullmoonInsuranceInfo.value?.insuranceAttachments || '').trim()
if (!urls) return []
return urls
.split(',')
.map((u) => u.trim())
.filter(Boolean)
.map((url, index) => {
const rawName = url.split('/').pop() || ''
const fileName = decodeURIComponent(rawName) || `附件${index + 1}`
return { id: `ins_${index}`, fileName, fileUrl: url, fileType: '' }
})
})
function fullmoonTodoStatusLabel(status: string) {
return status === '1' ? '已办保险' : '未办保险'
}
function fullmoonTodoStatusTagType(status: string) {
return status === '1' ? 'success' : 'warning'
}
function insuranceSubscribedLabel(val: string) {
return val === '1' ? '是' : '否'
}
function insuranceSubscribedTagType(val: string) {
return val === '1' ? 'success' : 'info'
}
function formatMoney(val: unknown) {
if (val === null || val === undefined || val === '') return '—'
const n = Number(val)
if (Number.isNaN(n)) return String(val)
return `¥${n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
}
function storageCertMakeStatusLabel(makeStatus: string) {
return makeStatus === '1' ? '已制作' : '未制作'
}
function storageCertMakeStatusTagType(makeStatus: string) {
return makeStatus === '1' ? 'success' : 'warning'
}
const dialogTitle = computed(() => {
const no = protocol.value?.protocolNo || props.listRow?.protocolNo
return no ? `查看协议单 — ${no}` : '查看协议单'
@@ -820,6 +1055,12 @@
return String(val)
}
function formatMetric(v: unknown) {
if (v === null || v === undefined || v === '') return '—'
const n = Number(v)
return Number.isFinite(n) ? String(n) : '—'
}
function formatYmd(val: unknown) {
const s = String(val || '').replace(/-/g, '')
if (s.length === 8) return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`
@@ -1249,7 +1490,7 @@
downloadAttachmentFile(file, { fullFileUrl })
}
async function loadLookups(data: Record<string, any>) {
async function loadBaseLookups() {
const [stRes, uRes, ecRes, siteRes] = await Promise.all([
fetchCellSampleTypeList({ page: 1, pageSize: 100 }),
fetchItemUnitList({ page: 1, pageSize: 100 }),
@@ -1260,7 +1501,9 @@
unitOptions.value = (uRes.list || []) as any
expressCompanyOptions.value = ecRes.list || []
siteOptions.value = siteRes.list || []
}
async function loadConditionalLookups(data: Record<string, any>) {
const ci = data.collectionInfo as Record<string, any> | null
const p = data.protocol as Record<string, any> | undefined
const tasks: Promise<void>[] = []
@@ -1291,10 +1534,22 @@
})
)
}
await Promise.all(tasks)
if (tasks.length) {
await Promise.all(tasks)
}
}
async function loadAuditRecords() {
async function finishLookups(data: Record<string, any>, baseLookupsPromise: Promise<void>) {
lookupLoading.value = true
try {
await baseLookupsPromise
await loadConditionalLookups(data)
} finally {
lookupLoading.value = false
}
}
async function loadAuditRecords(protocolNoHint?: string) {
if (!props.showAuditHistory || !props.protocolId) {
auditRecords.value = []
return
@@ -1302,7 +1557,12 @@
auditLoading.value = true
try {
const pno =
String(protocol.value?.protocolNo || props.listRow?.protocolNo || '').trim() || undefined
String(
protocolNoHint ||
protocol.value?.protocolNo ||
props.listRow?.protocolNo ||
''
).trim() || undefined
const res = await fetchCollectionProtocolAuditList({
page: 1,
pageSize: 100,
@@ -1319,16 +1579,35 @@
async function loadDetail() {
if (!props.protocolId) return
loading.value = true
detailLoading.value = true
lookupLoading.value = true
bundle.value = null
auditRecords.value = []
const protocolNoHint = String(props.listRow?.protocolNo || '').trim()
const baseLookupsPromise = loadBaseLookups().catch(() => {})
const auditEarlyPromise =
props.showAuditHistory && protocolNoHint
? loadAuditRecords(protocolNoHint)
: null
let data: Record<string, any> | null = null
try {
const data = await fetchCollectionProtocolDetail(props.protocolId)
bundle.value = data as Record<string, any>
await loadLookups(data as Record<string, any>)
await loadAuditRecords()
data = (await fetchCollectionProtocolDetail(props.protocolId)) as Record<string, any>
bundle.value = data
} finally {
loading.value = false
detailLoading.value = false
}
if (!data) {
lookupLoading.value = false
return
}
void finishLookups(data, baseLookupsPromise)
if (props.showAuditHistory && !auditEarlyPromise) {
void loadAuditRecords()
}
}
@@ -1345,6 +1624,9 @@
bundle.value = null
auditRecords.value = []
activeTab.value = 'basic'
detailLoading.value = false
lookupLoading.value = false
auditLoading.value = false
}
watch(
@@ -1396,59 +1678,77 @@
}
}
/* ---- KPI 摘要条 ---- */
/* ---- KPI 摘要条(统一薄荷绿通栏 + 图标徽章) ---- */
.kpi-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
gap: 0;
margin-bottom: 24px;
padding: 6px;
background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 52%, #ecfdf5 100%);
border: 1px solid #a7f3d0;
border-radius: 12px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
}
.kpi-card {
background: #fff;
border-radius: 12px;
padding: 18px 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
border-left: 3px solid #10b981;
transition: transform 0.2s, box-shadow 0.2s;
position: relative;
overflow: hidden;
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 18px;
background: transparent;
border-radius: 8px;
transition: background 0.2s;
&::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 60px;
height: 60px;
background: radial-gradient(
circle at top right,
rgba(16, 185, 129, 0.06),
transparent 70%
);
pointer-events: none;
&:not(:last-child) {
border-right: 1px solid rgba(167, 243, 208, 0.75);
}
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.1);
background: rgba(255, 255, 255, 0.55);
}
}
.kpi-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 10px;
background: #fff;
border: 1px solid #a7f3d0;
color: #059669;
font-size: 18px;
}
.kpi-card__main {
min-width: 0;
flex: 1;
}
.kpi-label {
font-size: 12px;
color: #94a3b8;
margin-bottom: 6px;
letter-spacing: 0.5px;
margin-bottom: 4px;
font-size: 11px;
font-weight: 500;
color: #64748b;
}
.kpi-value {
font-size: 16px;
font-size: 15px;
font-weight: 600;
color: #1e293b;
color: #0f172a;
display: flex;
align-items: center;
gap: 6px;
&--emphasis {
font-size: 16px;
font-weight: 700;
color: #047857;
letter-spacing: 0.02em;
}
}
/* ---- Section Card ---- */
@@ -1703,6 +2003,12 @@
}
}
:deep(.sample-card__section--qc) {
margin-top: 2px;
padding-top: 12px;
border-top: 1px dashed #e2e8f0;
}
.sample-reception-empty {
font-size: 13px;
color: #94a3b8;
@@ -1890,10 +2196,10 @@
/* ---- PDC Info Row ---- */
.pdc-info-row {
display: flex;
gap: 24px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px 20px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.pdc-info-item {
@@ -2237,8 +2543,8 @@
.refund-area-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px 20px;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px 20px;
.data-label {
font-size: 11px;
@@ -2441,11 +2747,19 @@
grid-template-columns: repeat(2, 1fr);
}
.kpi-strip .kpi-card {
border-right: none !important;
border-bottom: 1px solid rgba(167, 243, 208, 0.75);
&:nth-last-child(-n + 2) {
border-bottom: none;
}
}
.client-sub-cards {
grid-template-columns: 1fr;
}
.data-grid--3,
.data-grid--4 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -2460,11 +2774,18 @@
grid-template-columns: repeat(2, 1fr);
}
.kpi-strip .kpi-card:nth-last-child(-n + 2) {
border-bottom: 1px solid rgba(167, 243, 208, 0.75);
}
.kpi-strip .kpi-card:last-child {
border-bottom: none;
}
.client-sub-cards {
grid-template-columns: 1fr;
}
.data-grid--3,
.data-grid--4 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -2474,12 +2795,11 @@
}
.pdc-info-row {
flex-direction: column;
gap: 10px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.refund-area-grid {
grid-template-columns: 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.finance-area-grid {
@@ -45,6 +45,7 @@
v-loading="loading"
stripe
row-key="id"
:row-class-name="storageCertRowClassName"
>
<el-table-column
type="index"
@@ -56,9 +57,17 @@
/>
<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="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">
<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="入库日期" width="120" align="center" header-align="center">
<template #default="{ row }">{{ row.storageInDate || '—' }}</template>
</el-table-column>
<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">
@@ -67,7 +76,7 @@
</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">
<el-table-column label="操作" 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>上传储存证
@@ -95,7 +104,7 @@
<el-dialog
v-model="uploadDialogVisible"
title="上传储存证"
width="640px"
width="720px"
class="dialog-form-layout"
align-center
destroy-on-close
@@ -118,18 +127,74 @@
<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 label="储存证附件" class="form-col-span-2">
<div class="attachment-upload">
<div class="attachment-list">
<el-upload
ref="certAttachmentUploadRef"
:auto-upload="false"
:show-file-list="false"
:multiple="true"
accept="image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
@change="handleCertAttachmentChange"
>
<div class="upload-card">
<el-icon :size="28" color="#10b981"><Plus /></el-icon>
<span class="upload-card-text">上传附件</span>
</div>
</el-upload>
<div
v-for="(file, index) in certAttachments"
:key="file.id || index"
class="attachment-item"
:class="{ 'is-uploading': file._uploading }"
>
<div
class="attachment-preview"
@click="
isImageFile(file) && file.fileUrl
? handlePreviewAttachment(file)
: undefined
"
>
<template v-if="isImageFile(file) && file.fileUrl">
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
</template>
<template v-else-if="isImageFile(file) && !file.fileUrl">
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
</template>
<template v-else-if="isVideoFile(file)">
<div class="attachment-preview__icon attachment-preview__icon--video">
<el-icon :size="28"><VideoPlay /></el-icon>
</div>
</template>
<template v-else>
<div class="attachment-preview__icon attachment-preview__icon--doc">
<el-icon :size="28"><Document /></el-icon>
</div>
</template>
</div>
<div class="attachment-footer">
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
<div class="attachment-actions">
<el-icon
class="action-btn"
title="下载"
@click="handleDownloadAttachment(file)"
><Download
/></el-icon>
<el-icon
class="action-btn action-btn--danger"
title="删除"
@click="removeCertAttachment(index)"
><Delete
/></el-icon>
</div>
</div>
</div>
</div>
<span class="upload-hint">支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB</span>
</div>
</el-form-item>
<div class="form-grid-2">
<el-form-item label="制作日期" prop="makeDate">
@@ -157,84 +222,44 @@
</template>
</el-dialog>
<!-- 查看弹窗 -->
<el-dialog
v-model="viewDialogVisible"
title="查看储存证"
width="720px"
class="dialog-form-layout"
v-model="previewVisible"
title="文件预览"
width="800px"
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 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-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 v-else class="preview-container">
<video
:src="previewUrl"
controls
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
/>
</div>
<template #footer>
<el-button class="btn-ghost" @click="viewDialogVisible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
<!-- 查看弹窗(与采集协议单录入一致) -->
<ProtocolViewDialog
v-model="viewVisible"
:protocol-id="viewProtocolId"
:list-row="viewRow"
show-audit-history
show-storage-cert
/>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import type { UploadUserFile } from 'element-plus'
import {
Plus,
Close,
@@ -242,11 +267,32 @@
Search,
RefreshLeft,
View,
Upload
Upload,
Delete,
Document,
VideoPlay,
Loading,
Download
} from '@element-plus/icons-vue'
import ProtocolViewDialog from '../protocol/ProtocolViewDialog.vue'
import { fetchStorageCertList } from '@/api/admin'
import { fetchUpdateCollectionProtocol } from '@/api/collection-protocol'
import {
fetchUpdateCollectionProtocol,
type CollectionProtocolRow
} from '@/api/collection-protocol'
import { fetchUploadAttachment } from '@/api/business'
import { downloadAttachmentFile } from '@/utils/download-attachment'
import { maskIdCard } from '@/utils/mask-sensitive'
interface CertAttachmentItem {
id: string
fileName: string
fileUrl: string
fileSize?: number
fileSizeDisplay?: string
fileType?: string
_uploading?: boolean
}
defineOptions({ name: 'CellStorageStorageCert' })
@@ -264,29 +310,47 @@
const uploadDialogVisible = ref(false)
const uploadRow = ref<any>(null)
const uploadFormRef = ref()
const uploadFileList = ref<UploadUserFile[]>([])
const certAttachmentUploadRef = ref()
const certAttachments = ref<CertAttachmentItem[]>([])
const submitLoading = ref(false)
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const uploadForm = reactive({
makeDate: '',
remark: ''
})
const uploadFormRules = {
certImageUrls: [{ required: true, message: '请上传储存证图片', trigger: 'change' }],
makeDate: [{ required: true, message: '请选择制作日期', trigger: 'change' }]
}
const previewVisible = ref(false)
const previewUrl = ref('')
const previewType = ref<'image' | 'video'>('image')
// ---------------------------------------------------------------------------
// 查看弹窗
// 查看弹窗(与采集协议单录入一致)
// ---------------------------------------------------------------------------
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)
})
const viewVisible = ref(false)
const viewProtocolId = ref<string | null>(null)
const viewRow = ref<CollectionProtocolRow | null>(null)
/** 新生儿已满月且储存证未制作时标红(列表仅含已生效协议) */
function isStorageCertMakePendingRow(row: Record<string, unknown>) {
if (String(row.makeStatus ?? '0') === '1') return false
const delivery = String(row.newbornDeliveryTime || '').trim()
if (!delivery) return false
const deliveryDate = new Date(delivery.replace(/-/g, '/'))
if (Number.isNaN(deliveryDate.getTime())) return false
const fullmoon = new Date(deliveryDate)
fullmoon.setDate(fullmoon.getDate() + 30)
const today = new Date()
today.setHours(0, 0, 0, 0)
fullmoon.setHours(0, 0, 0, 0)
return fullmoon <= today
}
function storageCertRowClassName({ row }: { row: Record<string, unknown> }) {
return isStorageCertMakePendingRow(row) ? 'row-storage-cert-pending' : ''
}
// ---------------------------------------------------------------------------
// 数据加载
@@ -322,53 +386,155 @@
// ---------------------------------------------------------------------------
// 上传储存证
// ---------------------------------------------------------------------------
function fullFileUrl(suffix: string) {
if (!suffix) return ''
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/')) {
return suffix
}
return `${minioBaseUrl}/${suffix}`
}
function isImageFile(file: CertAttachmentItem) {
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) ||
(file.fileType || '').startsWith('image/')
)
}
function isVideoFile(file: CertAttachmentItem) {
const url = (file.fileUrl || '').toLowerCase()
const name = (file.fileName || '').toLowerCase()
return (
/\.(mp4|webm|ogg|mov|avi)(\?|$)/.test(url) ||
/\.(mp4|webm|ogg|mov|avi)$/.test(name) ||
(file.fileType || '').startsWith('video/')
)
}
function parseCertUrlsToAttachments(urls: string): CertAttachmentItem[] {
return urls
.split(',')
.map((u) => u.trim())
.filter(Boolean)
.map((url, index) => {
const rawName = url.split('/').pop() || ''
const fileName = decodeURIComponent(rawName) || `附件${index + 1}`
return {
id: `existing_${index}`,
fileName,
fileUrl: url,
fileType: '',
_uploading: false
}
})
}
function certAttachmentsToUrls() {
return certAttachments.value
.filter((f) => f.fileUrl && !f._uploading)
.map((f) => f.fileUrl)
.join(',')
}
async function handleCertAttachmentChange(uploadFile: any) {
const file = uploadFile.raw || uploadFile
if (!file) return
const maxSizeMb = 20
if (file.size / 1024 / 1024 > maxSizeMb) {
ElMessage.error(`文件大小不能超过 ${maxSizeMb}MB`)
certAttachmentUploadRef.value?.clearFiles()
return
}
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
certAttachments.value.push({
id: tempId,
fileName: file.name,
fileUrl: '',
fileSize: file.size,
fileSizeDisplay: `${(file.size / 1024).toFixed(1)} KB`,
fileType: file.type || '',
_uploading: true
})
const idx = certAttachments.value.length - 1
try {
const fd = new FormData()
fd.append('folder', 'storage-cert')
fd.append('file', file)
const res = await fetchUploadAttachment(fd)
certAttachments.value[idx] = {
...certAttachments.value[idx],
id: res.id || tempId,
fileName: file.name || res.fileName,
fileUrl: res.fileUrl,
fileSize: res.fileSize || file.size,
fileSizeDisplay: res.fileSizeDisplay || `${(file.size / 1024).toFixed(1)} KB`,
fileType: res.fileType || file.type || '',
_uploading: false
}
} catch {
ElMessage.error('文件上传失败')
certAttachments.value.splice(idx, 1)
} finally {
nextTick(() => certAttachmentUploadRef.value?.clearFiles())
}
}
function removeCertAttachment(index: number) {
certAttachments.value.splice(index, 1)
}
function handlePreviewAttachment(file: CertAttachmentItem) {
if (file._uploading) return
previewUrl.value = fullFileUrl(file.fileUrl)
previewType.value = isVideoFile(file) ? 'video' : 'image'
previewVisible.value = true
}
function handleDownloadAttachment(file: CertAttachmentItem) {
downloadAttachmentFile(file, { fullFileUrl })
}
function handleUpload(row: any) {
uploadRow.value = row
uploadFileList.value = []
uploadForm.makeDate = getTodayStr()
uploadForm.remark = ''
certAttachments.value = parseCertUrlsToAttachments(String(row.certImageUrls || ''))
uploadForm.makeDate = String(row.makeDate || '').trim() || getTodayStr()
uploadForm.remark = String(row.remark || '').trim()
uploadDialogVisible.value = true
}
async function handleSubmitUpload() {
// 手动校验图片
if (uploadFileList.value.length === 0) {
ElMessage.warning('请上传储存证图片')
if (certAttachments.value.some((f) => f._uploading)) {
ElMessage.warning('附件上传中,请稍候')
return
}
const valid = await uploadFormRef.value?.validate?.().catch(() => false)
if (!valid) return
const uploaded = certAttachments.value.filter((f) => f.fileUrl && !f._uploading)
const hasAttachments = uploaded.length > 0
if (hasAttachments) {
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, {
const protocolId = uploadRow.value?.protocolId || uploadRow.value?.id
await fetchUpdateCollectionProtocol(protocolId, {
storageCert: {
makeStatus: '1',
certImageUrls: finalUrls,
makeDate: uploadForm.makeDate,
makeStatus: hasAttachments ? '1' : '0',
certImageUrls: hasAttachments ? certAttachmentsToUrls() : '',
makeDate: hasAttachments ? uploadForm.makeDate : '',
remark: uploadForm.remark?.trim() || undefined
}
})
ElMessage.success('上传成功')
ElMessage.success(hasAttachments ? '上传成功' : '保存成功')
uploadDialogVisible.value = false
await loadData()
} catch {
@@ -378,12 +544,10 @@
}
}
// ---------------------------------------------------------------------------
// 查看储存证
// ---------------------------------------------------------------------------
function handleView(row: any) {
viewRow.value = row
viewDialogVisible.value = true
viewProtocolId.value = String(row.protocolId || row.id)
viewRow.value = row as CollectionProtocolRow
viewVisible.value = true
}
// ---------------------------------------------------------------------------
@@ -409,52 +573,184 @@
}
}
.view-info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px 24px;
margin-bottom: 20px;
.form-col-span-2 {
grid-column: 1 / -1;
}
.view-info-item {
.attachment-upload {
width: 100%;
}
.upload-card {
width: 140px;
height: 96px;
border: 1px dashed #a7f3d0;
border-radius: 8px;
background: #f0fdf4;
display: flex;
flex-direction: column;
gap: 4px;
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;
}
}
.view-label {
.upload-card-text {
font-size: 12px;
color: var(--mint-text-muted, #909399);
color: #10b981;
font-weight: 500;
line-height: 1;
}
.view-value {
font-size: 14px;
color: var(--el-text-color-primary, #303133);
word-break: break-all;
.upload-hint {
display: block;
font-size: 11px;
color: #94a3b8;
line-height: 1;
margin-top: 8px;
}
.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 {
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 12px;
}
.view-cert-img {
width: 120px;
.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;
border-radius: 6px;
border: 1px solid var(--el-border-color-lighter, #ebeef5);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
overflow: hidden;
cursor: pointer;
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;
&--video {
background: #ede9fe;
color: #7c3aed;
}
&--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;
min-height: calc(11px * 1.4 * 2);
}
.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;
}
}
.preview-container {
text-align: center;
}
.list-table :deep(tr.row-storage-cert-pending > td.el-table__cell) {
background-color: #fee2e2 !important;
color: #991b1b;
}
.list-table :deep(tr.row-storage-cert-pending:hover > td.el-table__cell) {
background-color: #fecaca !important;
}
</style>
@@ -22,8 +22,8 @@
<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>
<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>
@@ -371,7 +371,7 @@
</template>
<script setup lang="ts">
import { Plus, Close, Check, View, Edit, Delete, User, OfficeBuilding } 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, cleanRoomGradeTagType } from '@/utils/status-tag-registry'
@@ -22,8 +22,8 @@
<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>
<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>
@@ -166,7 +166,7 @@
</template>
<script setup lang="ts">
import { Plus, Close, Check, View, Calendar } from '@element-plus/icons-vue'
import { Plus, Close, Check, View, Calendar, 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 {
@@ -22,8 +22,8 @@
<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>
<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>
@@ -204,7 +204,7 @@
<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 } 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 {
@@ -22,8 +22,8 @@
<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>
<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>
@@ -155,7 +155,7 @@
<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 } 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 {
@@ -22,8 +22,8 @@
<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>
<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>
@@ -155,7 +155,7 @@
<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 } 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 {
@@ -22,8 +22,8 @@
<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>
<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>
@@ -1029,20 +1029,7 @@
<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
} 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'
@@ -22,8 +22,8 @@
<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>
<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>
@@ -276,7 +276,7 @@
<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 } 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 {
@@ -26,8 +26,8 @@
<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>
<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>
@@ -388,7 +388,7 @@
</template>
<script setup lang="ts">
import { Plus, Close, Check, View } from '@element-plus/icons-vue'
import { Plus, Close, Check, View, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
fetchConsumablePackList,
fetchConsumablePackDetail,
@@ -22,8 +22,8 @@
<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>
<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>
@@ -177,7 +177,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, ScaleToOriginal } 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 {
@@ -22,8 +22,8 @@
<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>
<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>
@@ -150,7 +150,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, ScaleToOriginal } 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 {
@@ -22,8 +22,8 @@
<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>
<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>
@@ -150,7 +150,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, ScaleToOriginal } 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 {
@@ -49,8 +49,8 @@
<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>
<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>
@@ -624,7 +624,7 @@
</template>
<script setup lang="ts">
import { Plus, Delete, Rank, Close, Check, CopyDocument } from '@element-plus/icons-vue'
import { Plus, Delete, Rank, Close, Check, CopyDocument, RefreshLeft, Search } from '@element-plus/icons-vue'
import { VueDraggable as draggable } from 'vue-draggable-plus'
import {
fetchProcessRouteList,
@@ -26,8 +26,8 @@
<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>
<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>
@@ -630,7 +630,7 @@
</template>
<script setup lang="ts">
import { Plus, Close, Check, Rank } from '@element-plus/icons-vue'
import { Plus, Close, Check, Rank, RefreshLeft, Search } from '@element-plus/icons-vue'
import Sortable from 'sortablejs'
import type { SortableEvent } from 'sortablejs'
import {
@@ -22,8 +22,8 @@
<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>
<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>
@@ -177,7 +177,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, ScaleToOriginal } 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 {
+3 -14
View File
@@ -22,8 +22,8 @@
<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>
<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>
@@ -302,18 +302,7 @@
<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
} 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 {
@@ -1,4 +1,5 @@
<!-- 物料变动记录 -->
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<header class="page-head">
@@ -44,6 +45,7 @@
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
</el-form-item>
</div>
@@ -61,29 +63,14 @@
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-end">
<el-tooltip content="列设置" placement="top">
<el-button class="btn-ghost" circle @click="showColumnSettings = true">
<el-icon><Setting /></el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="刷新" placement="top">
<el-button class="btn-ghost" circle @click="loadData">
<el-icon><Refresh /></el-icon>
</el-button>
</el-tooltip>
</div>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@row-dblclick="handleView"
>
<el-table-column
type="index"
@@ -93,44 +80,99 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="materialCode" label="物料编码" min-width="120" />
<el-table-column prop="materialName" label="物料名称" min-width="140" />
<el-table-column prop="changeType" label="变动类型" min-width="100" 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="changeType"
label="变动类型"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
:type="row.changeType === 'use' ? 'warning' : 'success'"
size="small"
>
<el-tag :type="row.changeType === 'use' ? 'warning' : 'success'" size="small">
{{ row.changeType === 'use' ? '使用' : '入库' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="changeQty" label="变动数量" min-width="120" align="right">
<el-table-column
prop="changeQty"
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="stockAfter" label="变动后库存" min-width="120" align="right" />
<el-table-column prop="qcTaskNo" label="关联任务号" min-width="160">
<el-table-column
prop="stockAfter"
label="变动后库存"
min-width="120"
align="right"
header-align="center"
/>
<el-table-column
prop="qcTaskNo"
label="关联任务号"
min-width="160"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ row.qcTaskNo || '-' }}
</template>
</el-table-column>
<el-table-column prop="sourceType" label="来源类型" min-width="120" align="center">
<el-table-column
prop="sourceType"
label="来源类型"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
:type="getSourceTagType(row.sourceType)"
size="small"
>
<el-tag :type="getSourceTagType(row.sourceType)" size="small">
{{ getSourceLabel(row.sourceType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
<el-table-column prop="createBy" label="操作人" min-width="100" />
<el-table-column prop="createTime" label="变动时间" min-width="170" />
<el-table-column
prop="remark"
label="备注"
min-width="140"
align="center"
header-align="center"
show-overflow-tooltip
/>
<el-table-column
prop="createBy"
label="操作人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
prop="createTime"
label="变动时间"
min-width="170"
align="center"
header-align="center"
/>
</el-table>
<div class="pagination list-pager-bar">
@@ -139,31 +181,36 @@
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
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>
<!-- 列设置抽屉 -->
<el-drawer v-model="showColumnSettings" title="列设置" size="300px">
<p>列设置功能预留</p>
</el-drawer>
<!-- 查看弹窗 -->
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewRow?.materialName || '变动记录详情'"
icon="Document"
:meta="viewRow?.createTime || ''"
:sections="viewSections"
/>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { Search, RefreshLeft, Refresh, Setting } from '@element-plus/icons-vue'
import { Search, RefreshLeft, View } 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: 'QcMaterialUsage' })
defineOptions({ name: 'QualityInspectionQcMaterialUsage' })
const route = useRoute()
@@ -180,7 +227,6 @@ const tableData = ref<QcMaterialUsage[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const materialOptions = ref<QcMaterialOption[]>([])
const showColumnSettings = ref(false)
function getSourceTagType(sourceType: string) {
if (sourceType === 'task_complete') return 'primary'
@@ -237,6 +283,44 @@ function handleReset() {
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: row.changeType === 'use' ? '使用' : '入库',
tagType: row.changeType === 'use' ? 'warning' : 'success'
},
{ label: '变动数量', value: row.changeQty < 0 ? String(row.changeQty) : `+${row.changeQty}` },
{ label: '变动后库存', value: String(row.stockAfter ?? 0) },
{ label: '关联任务号', value: row.qcTaskNo || '-' },
{
label: '来源类型',
value: getSourceLabel(row.sourceType),
tagType: getSourceTagType(row.sourceType) as any
},
{ label: '备注', value: row.remark || '—', span: true },
{ label: '操作人', value: row.createBy },
{ label: '变动时间', value: row.createTime }
]
}
]
})
function handleView(row: QcMaterialUsage) {
viewRow.value = row
viewVisible.value = true
}
onMounted(async () => {
await loadMaterialOptions()
if (route.query.materialId) {
@@ -28,8 +28,12 @@
<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>
<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>
@@ -42,14 +46,14 @@
</div>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增
新增物料
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length === 0"
@click="handleStockInFromToolbar"
>
入库
<el-icon><Upload /></el-icon>入库
</el-button>
</div>
</div>
@@ -62,6 +66,7 @@
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
@@ -105,12 +110,23 @@
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" 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="操作" width="260" fixed="right" align="center" header-align="center">
<el-table-column label="操作" width="400" 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="handleStockIn(row)">入库</el-button>
<el-button class="btn-op-item" @click="handleViewUsage(row)">详情</el-button>
<el-button 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 class="btn-op-item primary" @click="handleEdit(row)">
<el-icon><Edit /></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="handleViewUsage(row)">
<el-icon><List /></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>
@@ -240,14 +256,35 @@
</el-button>
</template>
</el-dialog>
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewRow?.materialName || '物料详情'"
:icon="Box"
:sections="viewSections"
/>
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check } from '@element-plus/icons-vue'
import {
Plus,
Close,
Check,
Search,
RefreshLeft,
View,
Edit,
Delete,
Upload,
List,
Box
} from '@element-plus/icons-vue'
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
import {
fetchQcMaterialList,
fetchCreateQcMaterial,
@@ -259,6 +296,7 @@
defineOptions({ name: 'QualityInspectionQcMaterial' })
const router = useRouter()
const searchForm = reactive({ keyword: '', status: '' })
const tableData = ref<QcMaterial[]>([])
const selectedRows = ref<QcMaterial[]>([])
@@ -305,6 +343,45 @@
qty: [{ required: true, message: '请输入入库数量', trigger: 'blur' }]
}
const viewVisible = ref(false)
const viewRow = ref<QcMaterial | null>(null)
const viewSections = computed<ViewSection[]>(() => {
if (!viewRow.value) return []
const row = viewRow.value
const stockLow = (row.stockQty ?? 0) <= (row.warningQty ?? 0)
return [
{
title: '基本信息',
fields: [
{ label: '物料编码', value: row.materialCode },
{ label: '物料名称', value: row.materialName },
{ label: '规格型号', value: row.specModel },
{ label: '单位', value: row.unit },
{
label: '状态',
value: row.status === '1' ? '启用' : '停用',
tagType: row.status === '1' ? 'success' : 'info'
},
{ label: '当前库存', value: String(row.stockQty ?? 0) },
{ label: '预警库存', value: String(row.warningQty ?? 0) },
{
label: '库存状态',
value: stockLow ? '库存预警' : '正常',
tagType: stockLow ? 'danger' : 'success'
}
]
},
{
title: '其他信息',
fields: [
{ label: '备注', value: row.remark, span: true },
{ label: '创建时间', value: row.createTime },
{ label: '更新时间', value: row.updateTime }
]
}
]
})
const handleSelectionChange = (rows: QcMaterial[]) => {
selectedRows.value = rows
}
@@ -439,9 +516,16 @@
}
}
// 查看变动记录
function handleView(row: QcMaterial) {
viewRow.value = row
viewVisible.value = true
}
function handleViewUsage(row: QcMaterial) {
window.location.hash = `/sqxp/quality-inspection/qc-material-usage?materialId=${row.id}`
router.push({
path: '/quality-inspection/qc-material-usage',
query: { materialId: row.id }
})
}
// 删除
@@ -49,7 +49,11 @@
</div>
<div class="filter-actions-row">
<span class="filter-hint">
{{ activeTab === 'pending' ? '勾选多个样本后可批量接收(同快递可一次登记,每项单独填写)。' : '双击行可查看接收详情(已接收)。' }}
{{
activeTab === 'pending'
? '勾选多个样本后可批量接收(同快递可一次登记,每项单独填写)。'
: '双击行可查看接收详情;也可点击「编辑」修改接收登记。'
}}
</span>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
@@ -73,7 +77,7 @@
:disabled="selectedPendingRows.length === 0"
@click="openBatchReceive"
>
批量接收
<el-icon><Files /></el-icon>批量接收
</el-button>
</div>
</div>
@@ -174,16 +178,19 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="140" 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'"
class="btn-op-item primary"
@click="openReceive(row)"
>
样本接收
<el-icon><CircleCheck /></el-icon>样本接收
</el-button>
<el-button v-else class="btn-op-item" @click="openView(row)">查看</el-button>
<template v-else>
<el-button class="btn-op-item" @click="openView(row)">查看</el-button>
<el-button class="btn-op-item" @click="openEdit(row)">编辑</el-button>
</template>
</template>
</el-table-column>
</el-table>
@@ -202,12 +209,13 @@
<el-dialog
v-model="receiveDialogVisible"
title="样本接收"
:title="receiveDialogTitle"
width="920px"
class="dialog-form-layout sample-reception-dialog"
align-center
destroy-on-close
:close-on-click-modal="false"
@closed="handleReceiveDialogClosed"
>
<div v-if="currentRow" class="sample-reception-readonly">
<h4 class="detail-section-title">样本信息</h4>
@@ -327,7 +335,7 @@
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="submitReceive">
<el-icon><Check /></el-icon>提交
<el-icon><Check /></el-icon>{{ receiveDialogMode === 'edit' ? '保存' : '提交' }}
</el-button>
</template>
</el-dialog>
@@ -538,12 +546,14 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { Close, Check, Search, RefreshLeft } from '@element-plus/icons-vue'
import { Close, Check, Search, RefreshLeft, CircleCheck, Files } from '@element-plus/icons-vue'
import { fetchUserOptions } from '@/api/admin'
import {
fetchSampleReceptionPendingList,
fetchSampleReceptionReceivedList,
fetchSampleReceptionSubmit,
fetchSampleReceptionUpdate,
fetchSampleReceptionDetail,
type SampleReceptionCheckResult
} from '@/api/sample-reception'
import {
@@ -575,6 +585,8 @@
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const receiveDialogVisible = ref(false)
const receiveDialogMode = ref<'create' | 'edit'>('create')
const editingReceptionId = ref('')
const viewDialogVisible = ref(false)
const submitLoading = ref(false)
const currentRow = ref<Record<string, unknown> | null>(null)
@@ -628,6 +640,10 @@
acceptConclusion: [{ required: true, message: '请选择是否接收', trigger: 'change' }]
}
const receiveDialogTitle = computed(() =>
receiveDialogMode.value === 'edit' ? '编辑样本接收' : '样本接收'
)
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
const viewCheckResults = computed((): SampleReceptionCheckResult[] => {
@@ -663,6 +679,76 @@
return Number.isFinite(n) ? String(n) : '-'
}
function apiFilesToFinanceItems(
files?: Array<{ id?: string; fileUrl?: string; fileName?: string; fileType?: string }>
): FinanceAttachmentItem[] {
if (!files?.length) return []
return files.map((f, i) => ({
id: String(f.id || `att_${i}`),
fileName: String(f.fileName || ''),
fileUrl: String(f.fileUrl || ''),
fileType: String(f.fileType || '')
}))
}
function applyCheckResultsFromDetail(results: SampleReceptionCheckResult[]) {
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
}
}
function populateReceiveFormFromDetail(detail: Record<string, unknown>) {
receiveForm.receptionTime = String(detail.receptionTime || '')
receiveForm.handlerUserId = String(detail.handlerUserId || '')
receiveForm.handlerName = String(detail.handlerName || '')
receiveForm.receiveTemperature = String(detail.receiveTemperature || '')
const vol = detail.volumeMl
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.remark = String(detail.remark || '')
const checkList = detail.checkResults
applyCheckResultsFromDetail(
Array.isArray(checkList) ? (checkList as SampleReceptionCheckResult[]) : []
)
if (Array.isArray(detail.receiveAttachmentFiles)) {
attachments.value = apiFilesToFinanceItems(
detail.receiveAttachmentFiles as Array<{
id?: string
fileUrl?: string
fileName?: string
fileType?: string
}>
)
} else {
const urls = String(detail.receiveAttachments || '')
.split(',')
.map((u) => u.trim())
.filter(Boolean)
attachments.value = urls.map((url, i) => {
const name = url.includes('/') ? url.split('/').pop() || `附件${i + 1}` : url
return { id: `url_${i}`, fileUrl: url, fileName: name }
})
}
}
function resetReceiveDialogState() {
receiveDialogMode.value = 'create'
editingReceptionId.value = ''
currentRow.value = null
}
function handleReceiveDialogClosed() {
resetReceiveDialogState()
}
function toOptionalMetric(v: number | undefined) {
if (v === null || v === undefined) return undefined
const n = Number(v)
@@ -941,6 +1027,8 @@
}
async function openReceive(row: Record<string, unknown>) {
receiveDialogMode.value = 'create'
editingReceptionId.value = ''
currentRow.value = row
if (!userOptions.value.length) {
await loadUserOptions()
@@ -953,6 +1041,40 @@
receiveDialogVisible.value = true
}
async function openEdit(row: Record<string, unknown>) {
const receptionId = String(row.id || '')
if (!receptionId) {
ElMessage.warning('缺少接收记录标识,无法编辑')
return
}
receiveDialogMode.value = 'edit'
editingReceptionId.value = receptionId
currentRow.value = row
if (!userOptions.value.length) {
await loadUserOptions()
} else {
ensureCurrentUserInOptions()
}
await loadActiveCheckItems()
let detail: Record<string, unknown> = row
try {
detail = (await fetchSampleReceptionDetail(receptionId)) as Record<string, unknown>
} catch {
/* 列表行已含主要字段,详情失败时回退 */
}
populateReceiveFormFromDetail(detail)
if (detail.handlerUserId) {
const hid = String(detail.handlerUserId)
if (!userOptions.value.some((o) => o.value === hid)) {
userOptions.value = [
{ label: String(detail.handlerName || hid), value: hid },
...userOptions.value
]
}
}
receiveDialogVisible.value = true
}
function openView(row: Record<string, unknown>) {
viewRow.value = row
viewDialogVisible.value = true
@@ -975,33 +1097,43 @@
return
}
submitLoading.value = true
const payload = {
receptionTime: receiveForm.receptionTime,
handlerUserId: receiveForm.handlerUserId,
handlerName: receiveForm.handlerName,
receiveTemperature: receiveForm.receiveTemperature || undefined,
volumeMl: toOptionalMetric(receiveForm.volumeMl),
netWeightG: toOptionalMetric(receiveForm.netWeightG),
checks: activeCheckItems.value.map((item) => ({
checkItemId: item.id,
checked: !!checkValues[item.id]
})),
acceptConclusion: receiveForm.acceptConclusion,
receiveAttachments: financeAttachmentsToUrlString(attachments.value) || undefined,
remark: receiveForm.remark || undefined
}
try {
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,
receptionTime: receiveForm.receptionTime,
handlerUserId: receiveForm.handlerUserId,
handlerName: receiveForm.handlerName,
receiveTemperature: receiveForm.receiveTemperature || undefined,
volumeMl: toOptionalMetric(receiveForm.volumeMl),
netWeightG: toOptionalMetric(receiveForm.netWeightG),
checks: activeCheckItems.value.map((item) => ({
checkItemId: item.id,
checked: !!checkValues[item.id]
})),
acceptConclusion: receiveForm.acceptConclusion,
receiveAttachments: financeAttachmentsToUrlString(attachments.value) || undefined,
remark: receiveForm.remark || undefined
})
receiveDialogVisible.value = false
pagination.page = 1
activeTab.value = 'received'
loadData()
if (receiveDialogMode.value === 'edit') {
await fetchSampleReceptionUpdate(editingReceptionId.value, payload)
ElMessage.success('接收信息已更新')
receiveDialogVisible.value = false
loadData()
} 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,
...payload
})
receiveDialogVisible.value = false
pagination.page = 1
activeTab.value = 'received'
loadData()
}
} finally {
submitLoading.value = false
}
@@ -28,31 +28,52 @@
<!-- KPI 摘要条 -->
<div class="kpi-strip">
<div class="kpi-card">
<div class="kpi-label">捐赠编号</div>
<div class="kpi-value">{{ dash(donation?.donationNo) }}</div>
</div>
<div class="kpi-card">
<div class="kpi-label">状态</div>
<div class="kpi-value">
<el-tag :type="donationStatusTagType(String(donation?.donationStatus || ''))" size="small">
{{ donationStatusLabel(String(donation?.donationStatus || '')) }}
</el-tag>
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><Document /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">捐赠编号</div>
<div class="kpi-value kpi-value--emphasis">{{ dash(donation?.donationNo) }}</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">母亲姓名</div>
<div class="kpi-value">{{ dash(party?.motherName) }}</div>
<div class="kpi-card__icon" aria-hidden="true">
<el-icon><CircleCheck /></el-icon>
</div>
<div class="kpi-card__main">
<div class="kpi-label">状态</div>
<div class="kpi-value">
<el-tag :type="donationStatusTagType(String(donation?.donationStatus || ''))" size="small">
{{ donationStatusLabel(String(donation?.donationStatus || '')) }}
</el-tag>
</div>
</div>
</div>
<div class="kpi-card">
<div class="kpi-label">捐赠日期</div>
<div class="kpi-value">{{ formatYmd(donation?.donationDate) }}</div>
<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(party?.motherName) }}</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">{{ formatYmd(donation?.donationDate) }}</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-grid data-grid--4">
<div class="data-item"
><div class="data-label">捐赠编号</div
><div class="data-value">{{ dash(donation?.donationNo) }}</div></div
@@ -82,60 +103,6 @@
><div class="data-value">{{ dash(donation?.createTime) }}</div></div
>
</div>
<!-- 状态变更记录 -->
<div v-if="hasAnyStatusChange" class="status-change-section">
<div class="status-change-title">状态变更记录</div>
<div class="data-grid data-grid--2">
<template v-if="donation?.effectiveTime">
<div class="data-item"
><div class="data-label">生效时间</div
><div class="data-value">{{ dash(donation.effectiveTime) }}</div></div
>
<div class="data-item"
><div class="data-label">生效描述</div
><div class="data-value data-value--muted">{{
dash(donation.effectiveDesc)
}}</div></div
>
</template>
<template v-if="donation?.expireTime">
<div class="data-item"
><div class="data-label">过期时间</div
><div class="data-value">{{ dash(donation.expireTime) }}</div></div
>
<div class="data-item"
><div class="data-label">过期描述</div
><div class="data-value data-value--muted">{{
dash(donation.expireDesc)
}}</div></div
>
</template>
<template v-if="donation?.voidTime">
<div class="data-item"
><div class="data-label">作废时间</div
><div class="data-value">{{ dash(donation.voidTime) }}</div></div
>
<div class="data-item"
><div class="data-label">作废描述</div
><div class="data-value data-value--muted">{{
dash(donation.voidDesc)
}}</div></div
>
</template>
<template v-if="donation?.convertTime">
<div class="data-item"
><div class="data-label">转细胞存储时间</div
><div class="data-value">{{ dash(donation.convertTime) }}</div></div
>
<div class="data-item"
><div class="data-label">转细胞存储描述</div
><div class="data-value data-value--muted">{{
dash(donation.convertDesc)
}}</div></div
>
</template>
</div>
</div>
<!-- 备注 -->
<div class="remark-row" v-if="donation?.remark">
<div class="data-item data-item--full"
@@ -143,6 +110,80 @@
><div class="data-value data-value--muted">{{ dash(donation?.remark) }}</div></div
>
</div>
<!-- 知情同意书 -->
<div v-if="consentAttachmentFiles.length" class="attachment-row">
<div class="data-label">知情同意书</div>
<ProtocolAttachmentReadonly
:files="consentAttachmentFiles"
@preview="openPreview"
@download="downloadFile"
/>
</div>
<!-- 其他附件 -->
<div v-if="otherAttachmentFiles.length" class="attachment-row">
<div class="data-label">其他附件</div>
<ProtocolAttachmentReadonly
:files="otherAttachmentFiles"
@download="downloadFile"
@preview="openPreview"
/>
</div>
</div>
<!-- 状态变更记录(独立区域) -->
<div id="view-section-status-change" class="section-card section-card--white">
<div class="section-title"><h3>状态变更记录</h3></div>
<el-empty v-if="!hasAnyStatusChange" description="暂无状态变更记录" :image-size="64" />
<div v-else class="data-grid data-grid--4">
<template v-if="donation?.effectiveTime">
<div class="data-item"
><div class="data-label">生效时间</div
><div class="data-value">{{ dash(donation.effectiveTime) }}</div></div
>
<div class="data-item"
><div class="data-label">生效描述</div
><div class="data-value data-value--muted">{{
dash(donation.effectiveDesc)
}}</div></div
>
</template>
<template v-if="donation?.expireTime">
<div class="data-item"
><div class="data-label">过期时间</div
><div class="data-value">{{ dash(donation.expireTime) }}</div></div
>
<div class="data-item"
><div class="data-label">过期描述</div
><div class="data-value data-value--muted">{{
dash(donation.expireDesc)
}}</div></div
>
</template>
<template v-if="donation?.voidTime">
<div class="data-item"
><div class="data-label">作废时间</div
><div class="data-value">{{ dash(donation.voidTime) }}</div></div
>
<div class="data-item"
><div class="data-label">作废描述</div
><div class="data-value data-value--muted">{{
dash(donation.voidDesc)
}}</div></div
>
</template>
<template v-if="donation?.convertTime">
<div class="data-item"
><div class="data-label">转细胞存储时间</div
><div class="data-value">{{ dash(donation.convertTime) }}</div></div
>
<div class="data-item"
><div class="data-label">转细胞存储描述</div
><div class="data-value data-value--muted">{{
dash(donation.convertDesc)
}}</div></div
>
</template>
</div>
</div>
<!-- 客户信息 -->
@@ -154,7 +195,7 @@
<div class="sub-card-header"
><span class="sub-card-icon">🤱</span><h4>母亲信息</h4></div
>
<div class="data-grid data-grid--2">
<div class="data-grid data-grid--4">
<div class="data-item"
><div class="data-label">关联客户</div
><div class="data-value">{{
@@ -194,7 +235,7 @@
<div class="sub-card-header"
><span class="sub-card-icon">👨</span><h4>父亲信息</h4></div
>
<div class="data-grid data-grid--2">
<div class="data-grid data-grid--4">
<div class="data-item"
><div class="data-label">父亲姓名</div
><div class="data-value">{{ dash(party?.fatherName) }}</div></div
@@ -214,7 +255,7 @@
<div class="sub-card-header"
><span class="sub-card-icon">👶</span><h4>新生儿信息</h4></div
>
<div class="data-grid data-grid--2">
<div class="data-grid data-grid--4">
<div class="data-item"
><div class="data-label">新生儿姓名</div
><div class="data-value">{{ dash(party?.newbornName) }}</div></div
@@ -263,7 +304,7 @@
<!-- 采集信息 -->
<div id="view-section-collection" class="section-card section-card--white">
<div class="section-title"><h3>采集信息</h3></div>
<div class="data-grid data-grid--3">
<div class="data-grid data-grid--4">
<div class="data-item"
><div class="data-label">采集地区</div
><div class="data-value">{{ regionDisplay }}</div></div
@@ -308,7 +349,7 @@
<div class="sample-card__body">
<div class="sample-card__section">
<div class="sample-card__section-label">样本信息</div>
<div class="data-grid data-grid--3">
<div class="data-grid data-grid--4">
<div class="data-item">
<div class="data-label">样本编码</div>
<div class="data-value data-value--code">{{ dash(row.sampleCode) }}</div>
@@ -334,7 +375,7 @@
class="sample-card__section sample-card__section--transport"
>
<div class="sample-card__section-label">运输信息</div>
<div class="data-grid data-grid--3">
<div class="data-grid data-grid--4">
<div class="data-item">
<div class="data-label">运输类型</div>
<div class="data-value">{{
@@ -357,6 +398,79 @@
</div>
</div>
</div>
<div class="sample-card__section sample-card__section--reception">
<div class="sample-card__section-label">样本接收信息</div>
<template v-if="row.sampleReception">
<div class="data-grid data-grid--4">
<div class="data-item">
<div class="data-label">接收时间</div>
<div class="data-value">{{ dash(row.sampleReception.receptionTime) }}</div>
</div>
<div class="data-item">
<div class="data-label">经手人</div>
<div class="data-value">{{ dash(row.sampleReception.handlerName) }}</div>
</div>
<div class="data-item">
<div class="data-label">接收温度(℃)</div>
<div class="data-value">
{{
row.sampleReception.receiveTemperature
? row.sampleReception.receiveTemperature + '℃'
: '—'
}}
</div>
</div>
<div class="data-item">
<div class="data-label">净重(g)</div>
<div class="data-value">{{ formatMetric(row.sampleReception.netWeightG) }}</div>
</div>
<div class="data-item">
<div class="data-label">结论</div>
<div class="data-value">
<el-tag
:type="acceptConclusionTagType(String(row.sampleReception.acceptConclusion || ''))"
size="small"
>
{{ acceptConclusionLabel(String(row.sampleReception.acceptConclusion || '')) }}
</el-tag>
</div>
</div>
<div class="data-item data-item--full">
<div class="data-label">检查项</div>
<div
v-if="sampleReceptionCheckList(row.sampleReception).length"
class="check-list check-list-view"
>
<el-checkbox
v-for="chk in sampleReceptionCheckList(row.sampleReception)"
:key="chk.itemCode + String(chk.checkItemId)"
:model-value="chk.checked"
disabled
>
{{ chk.itemName }}
</el-checkbox>
</div>
<div v-else class="data-value">—</div>
</div>
<div v-if="row.sampleReception.remark" class="data-item data-item--full">
<div class="data-label">备注</div>
<div class="data-value data-value--muted">{{ dash(row.sampleReception.remark) }}</div>
</div>
</div>
<div
v-if="sampleReceptionAttachmentFiles(row.sampleReception).length"
class="attachment-row sample-card__attachments"
>
<div class="data-label">接收附件</div>
<ProtocolAttachmentReadonly
:files="sampleReceptionAttachmentFiles(row.sampleReception)"
@preview="openPreview"
@download="downloadFile"
/>
</div>
</template>
<div v-else class="sample-reception-empty">暂未接收</div>
</div>
<SampleQcInfoCards :qc-info="row.qcInfo" :sample-reception="row.sampleReception" />
</div>
</div>
@@ -364,34 +478,6 @@
<el-empty v-else description="暂无样本" :image-size="48" />
</div>
<!-- 附件 -->
<div id="view-section-attachment" class="section-card section-card--white">
<div class="section-title"><h3>附件</h3></div>
<!-- 知情同意书 -->
<div v-if="consentAttachmentFiles.length" class="attachment-row">
<div class="data-label">知情同意书</div>
<ProtocolAttachmentReadonly
:files="consentAttachmentFiles"
@preview="openPreview"
@download="downloadFile"
/>
</div>
<!-- 其他附件 -->
<div v-if="otherAttachmentFiles.length" class="attachment-row">
<div class="data-label">其他附件</div>
<ProtocolAttachmentReadonly
:files="otherAttachmentFiles"
@download="downloadFile"
@preview="openPreview"
/>
</div>
<el-empty
v-if="!consentAttachmentFiles.length && !otherAttachmentFiles.length"
description="暂无附件"
:image-size="48"
/>
</div>
<!-- 审核记录 -->
<div
v-if="showAuditHistory"
@@ -486,7 +572,7 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import { Close } from '@element-plus/icons-vue'
import { Calendar, CircleCheck, Close, Document, User } from '@element-plus/icons-vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
import {
@@ -514,7 +600,9 @@
deliveryModeLabel,
sampleStatusLabel,
sampleStatusTagType,
transportTypeLabel
transportTypeLabel,
acceptConclusionLabel,
acceptConclusionTagType
} from '@/utils/status-tag-registry'
defineOptions({ name: 'DonationViewDialog' })
@@ -536,10 +624,10 @@
const baseFormTabs = [
{ key: 'basic', label: '基本信息' },
{ key: 'status-change', label: '状态变更记录' },
{ key: 'party', label: '客户信息' },
{ key: 'collection', label: '采集信息' },
{ key: 'sample', label: '样本及运输信息' },
{ key: 'attachment', label: '附件' }
{ key: 'sample', label: '样本及运输信息' }
] as const
const formTabs = computed(() => {
@@ -662,6 +750,12 @@
return String(val)
}
function formatMetric(v: unknown) {
if (v === null || v === undefined || v === '') return '—'
const n = Number(v)
return Number.isFinite(n) ? String(n) : '—'
}
function formatYmd(val: unknown) {
const s = String(val || '').replace(/-/g, '')
if (s.length === 8) return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`
@@ -696,6 +790,27 @@
return expressCompanyOptions.value.find((x) => x.id === id)?.expressName || dash(id)
}
function sampleReceptionCheckList(reception: Record<string, any>) {
const list = reception?.checkResults
return Array.isArray(list) ? list : []
}
function sampleReceptionAttachmentFiles(reception: Record<string, any>) {
const list = reception?.receiveAttachmentFiles
if (Array.isArray(list) && list.length) return list
const urls = String(reception?.receiveAttachments || '')
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: `recv_${i}`, fileUrl: url, fileName: name }
})
.filter(Boolean) as Array<Record<string, unknown>>
}
function sampleTransportExtra(transport: Record<string, any> | undefined) {
if (!transport?.transportType) return '—'
if (transport.transportType === 'kdys') {
@@ -897,57 +1012,77 @@
}
}
/* ---- KPI 摘要条 ---- */
/* ---- KPI 摘要条(与采集协议单查看弹窗一致:一行四列 + 薄荷绿通栏) ---- */
.kpi-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
gap: 0;
margin-bottom: 24px;
padding: 6px;
background: linear-gradient(135deg, #ecfdf5 0%, #f8fafc 52%, #ecfdf5 100%);
border: 1px solid #a7f3d0;
border-radius: 12px;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
}
.kpi-card {
background: #fff;
border-radius: 12px;
padding: 18px 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
border-left: 3px solid #10b981;
transition:
transform 0.2s,
box-shadow 0.2s;
position: relative;
overflow: hidden;
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 18px;
background: transparent;
border-radius: 8px;
transition: background 0.2s;
&::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 60px;
height: 60px;
background: radial-gradient(circle at top right, rgba(16, 185, 129, 0.06), transparent 70%);
pointer-events: none;
&:not(:last-child) {
border-right: 1px solid rgba(167, 243, 208, 0.75);
}
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.1);
background: rgba(255, 255, 255, 0.55);
}
}
.kpi-card__icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 10px;
background: #fff;
border: 1px solid #a7f3d0;
color: #059669;
font-size: 18px;
}
.kpi-card__main {
min-width: 0;
flex: 1;
}
.kpi-label {
font-size: 12px;
color: #94a3b8;
margin-bottom: 6px;
letter-spacing: 0.5px;
margin-bottom: 4px;
font-size: 11px;
font-weight: 500;
color: #64748b;
}
.kpi-value {
font-size: 16px;
font-size: 15px;
font-weight: 600;
color: #1e293b;
color: #0f172a;
display: flex;
align-items: center;
gap: 6px;
&--emphasis {
font-size: 16px;
font-weight: 700;
color: #047857;
letter-spacing: 0.02em;
}
}
/* ---- Section Card ---- */
@@ -1200,13 +1335,68 @@
}
.sample-card__section {
&--transport {
&--transport,
&--reception {
margin-top: 2px;
padding-top: 12px;
border-top: 1px dashed #e2e8f0;
}
}
:deep(.sample-card__section--qc) {
margin-top: 2px;
padding-top: 12px;
border-top: 1px dashed #e2e8f0;
}
.sample-reception-empty {
font-size: 13px;
color: #94a3b8;
}
.check-list {
display: flex;
flex-wrap: wrap;
gap: 12px 20px;
}
.check-list-view {
pointer-events: none;
:deep(.el-checkbox.is-disabled) {
opacity: 1;
cursor: default;
.el-checkbox__label {
color: var(--el-text-color-regular);
}
.el-checkbox__input.is-disabled {
opacity: 1;
cursor: default;
.el-checkbox__inner {
background-color: var(--el-checkbox-bg-color);
border-color: var(--el-checkbox-input-border-color);
cursor: default;
}
&.is-checked .el-checkbox__inner {
background-color: var(--el-checkbox-checked-bg-color);
border-color: var(--el-checkbox-checked-input-border-color);
&::after {
border-color: var(--el-checkbox-checked-icon-color);
}
}
}
}
}
.sample-card__attachments {
margin-top: 12px;
}
.sample-card__section-label {
font-size: 11px;
font-weight: 600;
@@ -1232,6 +1422,15 @@
grid-template-columns: repeat(2, 1fr);
}
.kpi-strip .kpi-card {
border-right: none !important;
border-bottom: 1px solid rgba(167, 243, 208, 0.75);
&:nth-last-child(-n + 2) {
border-bottom: none;
}
}
.client-sub-cards {
grid-template-columns: 1fr;
}
@@ -1247,6 +1446,14 @@
grid-template-columns: repeat(2, 1fr);
}
.kpi-strip .kpi-card:nth-last-child(-n + 2) {
border-bottom: 1px solid rgba(167, 243, 208, 0.75);
}
.kpi-strip .kpi-card:last-child {
border-bottom: none;
}
.client-sub-cards {
grid-template-columns: 1fr;
}
@@ -29,8 +29,8 @@
<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>
<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>
@@ -440,7 +440,7 @@
import { ref, reactive, onMounted, nextTick, watch, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import { Plus, Search, Rank, Close, Check, View, Files } from '@element-plus/icons-vue'
import { Plus, Search, Rank, Close, Check, View, Files, RefreshLeft } from '@element-plus/icons-vue'
import { VueDraggable as draggable } from 'vue-draggable-plus'
import type { UploadFile } from 'element-plus'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
+3 -3
View File
@@ -22,8 +22,8 @@
<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>
<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>
@@ -225,7 +225,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, OfficeBuilding } 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,
+3 -5
View File
@@ -25,10 +25,8 @@
<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>
<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>
@@ -356,7 +354,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Collection } 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 {
+3 -3
View File
@@ -22,8 +22,8 @@
<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>
<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>
@@ -150,7 +150,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Van } from '@element-plus/icons-vue'
import { Plus, Close, Check, View, Van, 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 {
+3 -3
View File
@@ -29,8 +29,8 @@
<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>
<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>
@@ -176,7 +176,7 @@
</template>
<script setup lang="ts">
import { Close, Check } from '@element-plus/icons-vue'
import { Close, Check, RefreshLeft, Search } from '@element-plus/icons-vue'
import { leaveStatusLabel, leaveStatusTagType } from '@/utils/status-tag-registry'
import {
fetchLeaveList,
+3 -3
View File
@@ -21,8 +21,8 @@
<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>
<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>
@@ -270,7 +270,7 @@
<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 } from '@element-plus/icons-vue'
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,
+3 -3
View File
@@ -22,8 +22,8 @@
<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>
<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>
@@ -272,7 +272,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, User, View, Briefcase } from '@element-plus/icons-vue'
import { Plus, Close, Check, User, View, Briefcase, RefreshLeft, Search } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
+3 -17
View File
@@ -44,8 +44,8 @@
<div class="filter-actions-row">
<span class="filter-hint">支持按商品名称、SKU 与状态筛选。</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>
<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>
@@ -791,21 +791,7 @@
type UploadFile
} from 'element-plus'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
Plus,
Delete,
VideoPlay,
Document,
Picture,
Setting,
Search,
CopyDocument,
Rank,
Loading,
Operation,
Close,
Check
} from '@element-plus/icons-vue'
import { Plus, Delete, VideoPlay, Document, Picture, Setting, Search, CopyDocument, Rank, Loading, Operation, Close, Check, RefreshLeft } from '@element-plus/icons-vue'
import { VueDraggable as draggable } from 'vue-draggable-plus'
import ProductViewDialog from '@/components/business/product-view-dialog/index.vue'
import {
+3 -3
View File
@@ -22,8 +22,8 @@
<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>
<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>
@@ -368,7 +368,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, DataAnalysis, View, Stamp } from '@element-plus/icons-vue'
import { Plus, Close, Check, DataAnalysis, View, Stamp, RefreshLeft, Search } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
+3 -3
View File
@@ -22,8 +22,8 @@
<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>
<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>
@@ -229,7 +229,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, Location } from '@element-plus/icons-vue'
import { Plus, Close, Check, View, Location, RefreshLeft, Search } from '@element-plus/icons-vue'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import {
+3 -3
View File
@@ -26,8 +26,8 @@
<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>
<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>
@@ -226,7 +226,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, OfficeBuilding } 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 { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
+3 -3
View File
@@ -22,8 +22,8 @@
<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>
<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>
@@ -150,7 +150,7 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, ScaleToOriginal } 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 {
+3 -3
View File
@@ -51,8 +51,8 @@
<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>
<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>
@@ -215,7 +215,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Close, Check, View, UserFilled } from '@element-plus/icons-vue'
import { Plus, Close, Check, View, UserFilled, RefreshLeft, Search } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import {
fetchUserList,