提交
This commit is contained in:
1 parent
f269a994f4
commit
93cd506a90
71 files changed
+8297
-2207
No files matched your search
@@ -132,6 +132,13 @@ export function fetchUploadAttachment(data: FormData) {
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchAttachmentDownloadUrl(data: { fileUrl: string; fileName?: string }) {
|
||||
return request.post<{ downloadUrl: string }>({
|
||||
url: '/web/admin/attachment/download-url',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUploadProductImage(data: FormData) {
|
||||
return request.post({
|
||||
url: '/web/admin/products/upload-image',
|
||||
|
||||
@@ -21,6 +21,7 @@ export type QcTask = {
|
||||
feeAmount?: number
|
||||
taskStatus: string
|
||||
thirdPartyAttachments?: string
|
||||
attachmentFiles?: Array<{ id?: string; fileUrl: string; fileName?: string; fileType?: string }>
|
||||
completedTime?: string
|
||||
completedBy?: string
|
||||
remark?: string
|
||||
|
||||
@@ -100,7 +100,7 @@ export function fetchSubmitDonationAudit(
|
||||
// 状态变更
|
||||
export function fetchDonationStatusChange(
|
||||
donationId: string,
|
||||
data: { donationStatus: string; desc?: string; time?: string }
|
||||
data: { newStatus: string; desc?: string; timeVal?: string }
|
||||
) {
|
||||
return request.post({
|
||||
url: `/web/admin/research-donations/${donationId}/status-change`,
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
VideoPlay
|
||||
} from '@element-plus/icons-vue'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import type { FinanceAttachmentItem } from './finance-attachments'
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -135,13 +136,7 @@
|
||||
}
|
||||
|
||||
function downloadFile(file: FinanceAttachmentItem) {
|
||||
if (!file.fileUrl) return
|
||||
const a = document.createElement('a')
|
||||
a.href = fullFileUrl(file.fileUrl)
|
||||
a.download = file.fileName || 'download'
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.click()
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
function removeAttachment(index: number) {
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<slot name="append" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
|
||||
@@ -142,13 +142,6 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
path: 'exhibition-appointment',
|
||||
component: '/operations/exhibition-appointment/index',
|
||||
meta: { title: '展馆预约', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '60000000000000000000000000000005',
|
||||
name: 'OperationsDemo',
|
||||
path: 'demo',
|
||||
component: '/operations/demo/index',
|
||||
meta: { title: 'demo', keepAlive: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -218,7 +211,14 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
name: 'CellStorageProtocol',
|
||||
path: 'protocol',
|
||||
component: '/cell-storage/protocol/index',
|
||||
meta: { title: '采集协议单', keepAlive: true }
|
||||
meta: { title: '采集协议单录入', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000003',
|
||||
name: 'CellStorageProtocolAudit',
|
||||
path: 'protocol-audit',
|
||||
component: '/cell-storage/protocol-audit/index',
|
||||
meta: { title: '采集协议单管理', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000007',
|
||||
@@ -227,13 +227,6 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
component: '/cell-storage/payment-info/index',
|
||||
meta: { title: '缴费信息', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000003',
|
||||
name: 'CellStorageProtocolAudit',
|
||||
path: 'protocol-audit',
|
||||
component: '/cell-storage/protocol-audit/index',
|
||||
meta: { title: '采集协议单审核', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '50000000000000000000000000000004',
|
||||
name: 'CellStoragePaymentConfirm',
|
||||
@@ -267,14 +260,14 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
name: 'ResearchDonationList',
|
||||
path: 'list',
|
||||
component: '/research-donation/list/index',
|
||||
meta: { title: '捐赠协议单', keepAlive: true }
|
||||
meta: { title: '捐赠协议单录入', keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '60000000000000000000000000000003',
|
||||
name: 'ResearchDonationAudit',
|
||||
path: 'audit',
|
||||
component: '/research-donation/audit/index',
|
||||
meta: { title: '捐赠协议审核', keepAlive: true }
|
||||
meta: { title: '捐赠协议单管理', keepAlive: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -422,6 +415,13 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
component: '/production/consumable/index',
|
||||
meta: { title: '耗材管理', keepAlive: true, menuCode: 'production:consumable:list' }
|
||||
},
|
||||
{
|
||||
id: '90000000000000000000000000000060',
|
||||
name: 'ProductionConsumablePack',
|
||||
path: 'consumable-pack',
|
||||
component: '/production/consumable-pack/index',
|
||||
meta: { title: '耗材包管理', keepAlive: true, menuCode: 'production:consumable-pack:list' }
|
||||
},
|
||||
{
|
||||
id: '90000000000000000000000000000020',
|
||||
name: 'ProductionReagent',
|
||||
@@ -429,13 +429,6 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
component: '/production/reagent/index',
|
||||
meta: { title: '试剂管理', keepAlive: true, menuCode: 'production:reagent:list' }
|
||||
},
|
||||
{
|
||||
id: '90000000000000000000000000000030',
|
||||
name: 'ProductionSop',
|
||||
path: 'sop',
|
||||
component: '/production/sop/index',
|
||||
meta: { title: 'SOP文档', keepAlive: true, menuCode: 'production:sop:list' }
|
||||
},
|
||||
{
|
||||
id: '90000000000000000000000000000040',
|
||||
name: 'ProductionProcessCategory',
|
||||
@@ -450,19 +443,19 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
component: '/production/output-type/index',
|
||||
meta: { title: '工艺产出物类型', keepAlive: true, menuCode: 'production:output-type:list' }
|
||||
},
|
||||
{
|
||||
id: '90000000000000000000000000000060',
|
||||
name: 'ProductionConsumablePack',
|
||||
path: 'consumable-pack',
|
||||
component: '/production/consumable-pack/index',
|
||||
meta: { title: '耗材包管理', keepAlive: true, menuCode: 'production:consumable-pack:list' }
|
||||
},
|
||||
{
|
||||
id: '90000000000000000000000000000070',
|
||||
name: 'ProductionProcess',
|
||||
path: 'process',
|
||||
component: '/production/process/index',
|
||||
meta: { title: '工艺库管理', keepAlive: true, menuCode: 'production:process:list' }
|
||||
},
|
||||
{
|
||||
id: '90000000000000000000000000000030',
|
||||
name: 'ProductionSop',
|
||||
path: 'sop',
|
||||
component: '/production/sop/index',
|
||||
meta: { title: 'SOP文档', keepAlive: true, menuCode: 'production:sop:list' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Vendored
+2
-2
@@ -542,7 +542,7 @@ declare namespace Api {
|
||||
id: string
|
||||
protocolNo: string
|
||||
issued: number
|
||||
/** 是否已在采集协议单主表占用(与号池「是否下发」无关) */
|
||||
/** 是否已在采集协议单或科研捐赠单中占用(与号池「是否下发」无关) */
|
||||
usedInProtocol?: boolean
|
||||
issuedUserId: string
|
||||
issuedUserName: string
|
||||
@@ -561,7 +561,7 @@ declare namespace Api {
|
||||
sampleCode: string
|
||||
sampleCodeType: string
|
||||
issued: number
|
||||
/** 是否已在协议单样本表中占用(与号池「是否下发」无关) */
|
||||
/** 是否已在采集协议单或科研捐赠单样本表中占用(与号池「是否下发」无关) */
|
||||
usedInProtocol?: boolean
|
||||
issuedUserId: string
|
||||
issuedUserName: string
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { fetchAttachmentDownloadUrl } from '@/api/business'
|
||||
|
||||
export type AttachmentDownloadItem = {
|
||||
fileUrl?: string
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
function triggerAnchorDownload(href: string, fileName: string) {
|
||||
const a = document.createElement('a')
|
||||
a.href = href
|
||||
a.download = fileName
|
||||
a.rel = 'noopener'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
}
|
||||
|
||||
/** 附件下载:优先走后端预签名 URL,保证跨域场景下保留原始文件名与后缀。 */
|
||||
export async function downloadAttachmentFile(
|
||||
file: AttachmentDownloadItem,
|
||||
options?: { fullFileUrl?: (url: string) => string }
|
||||
) {
|
||||
const fileUrl = String(file.fileUrl || '').trim()
|
||||
if (!fileUrl) return
|
||||
const fileName = String(file.fileName || '').trim() || fileUrl.split('/').pop() || 'download'
|
||||
|
||||
try {
|
||||
const res = await fetchAttachmentDownloadUrl({ fileUrl, fileName })
|
||||
if (res?.downloadUrl) {
|
||||
triggerAnchorDownload(res.downloadUrl, fileName)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// 预签名失败时回退直连
|
||||
}
|
||||
|
||||
const resolveUrl = options?.fullFileUrl || ((url: string) => url)
|
||||
const href = resolveUrl(fileUrl)
|
||||
if (!href) {
|
||||
ElMessage.error('无法下载附件')
|
||||
return
|
||||
}
|
||||
triggerAnchorDownload(href, fileName)
|
||||
}
|
||||
@@ -22,5 +22,8 @@ export const fullscreenLoadingText = {
|
||||
save: '正在保存,请稍候…',
|
||||
delete: '正在删除,请稍候…',
|
||||
batchDelete: '正在批量删除,请稍候…',
|
||||
batchAudit: '正在提交审核,请稍候…'
|
||||
batchAudit: '正在提交审核,请稍候…',
|
||||
activate: '正在生效,请稍候…',
|
||||
voidAgreement: '正在作废,请稍候…',
|
||||
addItems: '正在添加,请稍候…'
|
||||
} as const
|
||||
@@ -242,6 +242,7 @@
|
||||
type FinanceAttachmentItem
|
||||
} from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import {
|
||||
formatPaymentAmount,
|
||||
formatPaymentDueDate,
|
||||
@@ -304,14 +305,7 @@
|
||||
}
|
||||
|
||||
function handleAttachmentDownload(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = fullFileUrl(url)
|
||||
a.download = String(file.fileName || 'download')
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.click()
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -738,7 +738,7 @@
|
||||
|
||||
<!-- Tab 5: 缴费信息 -->
|
||||
<div id="form-section-payment" class="form-tab-pane">
|
||||
<div class="section-card section-card--gray">
|
||||
<div class="section-card">
|
||||
<div class="section-title">
|
||||
<div class="section-title-left"><h3>缴费信息</h3></div>
|
||||
</div>
|
||||
@@ -1194,6 +1194,7 @@
|
||||
type CollectionProtocolRow
|
||||
} from '@/api/collection-protocol'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import {
|
||||
fetchCustomerList,
|
||||
fetchUserList,
|
||||
@@ -2056,7 +2057,7 @@
|
||||
if (!isEdit.value && formData.protocolNoId) {
|
||||
const picked = protocolNoOptions.value.find((x) => x.id === formData.protocolNoId)
|
||||
if (picked?.usedInProtocol) {
|
||||
ElMessage.warning(`协议号「${formData.protocolNo}」已存在协议单,请更换其他协议号`)
|
||||
ElMessage.warning(`协议号「${formData.protocolNo}」已被占用,请更换其他协议号`)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -2066,7 +2067,7 @@
|
||||
const sc = sampleCodeOptions.value.find((x) => x.id === sampleRow.sampleCodeId)
|
||||
if (sc?.usedInProtocol) {
|
||||
ElMessage.warning(
|
||||
`样本编码「${sampleRow.sampleCode || sc.sampleCode}」已被其他协议单使用,请更换`
|
||||
`样本编码「${sampleRow.sampleCode || sc.sampleCode}」已被占用,请更换`
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -2660,7 +2661,7 @@
|
||||
|
||||
/* ====== 协议号下拉检索 ====== */
|
||||
function protocolNoOptionLabel(p: Api.BizCollectionProtocolNo.ProtocolNoItem) {
|
||||
if (p.usedInProtocol) return `${p.protocolNo}(已建单)`
|
||||
if (p.usedInProtocol) return `${p.protocolNo}(已占用)`
|
||||
if (p.issued) return `${p.protocolNo}(已下发)`
|
||||
return p.protocolNo
|
||||
}
|
||||
@@ -2706,7 +2707,7 @@
|
||||
|
||||
/* ====== 样本编码选择 ====== */
|
||||
function sampleCodeOptionLabel(sc: Api.BizSampleCode.SampleCodeItem) {
|
||||
if (sc.usedInProtocol) return `${sc.sampleCode}(已建单)`
|
||||
if (sc.usedInProtocol) return `${sc.sampleCode}(已占用)`
|
||||
if (sc.issued) return `${sc.sampleCode}(已下发)`
|
||||
return sc.sampleCode
|
||||
}
|
||||
@@ -2894,15 +2895,7 @@
|
||||
}
|
||||
|
||||
function handleDownloadAttachment(file: any) {
|
||||
const url = fullFileUrl(file.fileUrl)
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = file.fileName || ''
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
/* ====== 弹窗打开时自动初始化 ====== */
|
||||
@@ -2998,10 +2991,6 @@
|
||||
&--green {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
&--gray {
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
if (props.actionType === 'toResearch') {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定将该协议转为科研捐赠吗?转科研后将创建新的科研捐赠记录。',
|
||||
'确定将该协议转为科研捐赠吗?将创建新的科研捐赠记录,初始协议号与样本编码与源单一致。',
|
||||
'转科研确认',
|
||||
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 缴费信息 -->
|
||||
<div id="view-section-payment" class="section-card section-card--gray">
|
||||
<div id="view-section-payment" class="section-card">
|
||||
<div class="section-title"><h3>缴费信息</h3></div>
|
||||
<!-- 缴费主表指标卡 -->
|
||||
<div class="payment-summary">
|
||||
@@ -664,6 +664,7 @@
|
||||
fetchHospitalList,
|
||||
fetchUserList
|
||||
} from '@/api/admin'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -1343,15 +1344,7 @@
|
||||
}
|
||||
|
||||
function downloadFile(file: Record<string, any>) {
|
||||
const url = fullFileUrl(file.fileUrl)
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = file.fileName || ''
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
async function loadLookups(data: Record<string, any>) {
|
||||
@@ -1606,10 +1599,6 @@
|
||||
&--green {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
&--gray {
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -259,6 +259,7 @@
|
||||
type FinanceAttachmentItem
|
||||
} from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
|
||||
import { maskIdCard } from '@/utils/mask-sensitive'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import {
|
||||
formatPaymentAmount,
|
||||
formatPaymentDueDate,
|
||||
@@ -321,14 +322,7 @@
|
||||
}
|
||||
|
||||
function handleAttachmentDownload(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = fullFileUrl(url)
|
||||
a.download = String(file.fileName || 'download')
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.click()
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
|
||||
@@ -425,10 +425,16 @@
|
||||
|
||||
// --- Delete ---
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm('确定要删除该价格记录吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteCellProductPrice(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该价格记录吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteCellProductPrice(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
// --- Batch delete ---
|
||||
|
||||
@@ -293,10 +293,16 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.CellProductSpec.CellProductSpecItem) {
|
||||
await ElMessageBox.confirm('确定要删除该规格吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteCellProductSpec(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该规格吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteCellProductSpec(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
|
||||
@@ -293,10 +293,16 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.CellProductType.CellProductTypeItem) {
|
||||
await ElMessageBox.confirm('确定要删除该类型吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteCellProductType(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该类型吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteCellProductType(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
<el-empty v-else description="暂无邮寄地址" :image-size="48" />
|
||||
</div>
|
||||
|
||||
<div id="view-section-agreement" class="section-card section-card--gray">
|
||||
<div id="view-section-agreement" class="section-card">
|
||||
<div class="section-title"><h3>协议信息</h3></div>
|
||||
<div v-if="agreementList.length" class="agreement-cards">
|
||||
<div
|
||||
@@ -235,6 +235,7 @@
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import {
|
||||
fetchChannelInvoices,
|
||||
fetchChannelMailAddresses,
|
||||
@@ -301,7 +302,12 @@
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const protocolAttachmentFiles = computed(() => {
|
||||
const urls = String(channel.value?.protocolAttachments || '').trim()
|
||||
const row = channel.value
|
||||
if (!row) return []
|
||||
if (Array.isArray(row.attachmentFiles) && row.attachmentFiles.length) {
|
||||
return row.attachmentFiles as Array<Record<string, any>>
|
||||
}
|
||||
const urls = String(row.protocolAttachments || '').trim()
|
||||
if (!urls) return []
|
||||
return urls
|
||||
.split(',')
|
||||
@@ -414,15 +420,7 @@
|
||||
}
|
||||
|
||||
function downloadFile(file: Record<string, any>) {
|
||||
const url = fullFileUrl(file.fileUrl)
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = file.fileName || ''
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
async function loadSubTables() {
|
||||
@@ -586,10 +584,6 @@
|
||||
&--green {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
&--gray {
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
|
||||
@@ -314,13 +314,28 @@
|
||||
新增
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="invoiceList.length" class="sub-cards">
|
||||
<div
|
||||
v-if="invoiceList.length"
|
||||
class="sub-cards"
|
||||
v-loading="!!deletingInvoiceId"
|
||||
element-loading-text="删除中…"
|
||||
>
|
||||
<div v-for="(row, $index) in invoiceList" :key="row.id || $index" class="sub-card">
|
||||
<div class="sub-card-header">
|
||||
<h4>{{ row.companyName || '未命名' }}</h4>
|
||||
<div class="sub-card-actions">
|
||||
<el-button class="btn-op-item primary" link size="small" @click="handleEditInvoice(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" type="danger" link size="small" @click="handleDeleteInvoice(row)">删除</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
:loading="deletingInvoiceId === row.id"
|
||||
:disabled="!!deletingInvoiceId"
|
||||
@click="handleDeleteInvoice(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-card-fields">
|
||||
@@ -366,13 +381,28 @@
|
||||
新增
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="mailList.length" class="sub-cards">
|
||||
<div
|
||||
v-if="mailList.length"
|
||||
class="sub-cards"
|
||||
v-loading="!!deletingMailId"
|
||||
element-loading-text="删除中…"
|
||||
>
|
||||
<div v-for="(row, $index) in mailList" :key="row.id || $index" class="sub-card">
|
||||
<div class="sub-card-header">
|
||||
<h4>{{ row.mailAddress || '未命名' }}</h4>
|
||||
<div class="sub-card-actions">
|
||||
<el-button class="btn-op-item primary" link size="small" @click="handleEditMail(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" type="danger" link size="small" @click="handleDeleteMail(row)">删除</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
:loading="deletingMailId === row.id"
|
||||
:disabled="!!deletingMailId"
|
||||
@click="handleDeleteMail(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sub-card-fields">
|
||||
@@ -524,7 +554,12 @@
|
||||
新增协议
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="agreementList.length" class="protocol-cards">
|
||||
<div
|
||||
v-if="agreementList.length"
|
||||
class="protocol-cards"
|
||||
v-loading="!!agreementOperatingId"
|
||||
element-loading-text="处理中…"
|
||||
>
|
||||
<div
|
||||
v-for="(agr, $index) in agreementList"
|
||||
:key="agr.id || $index"
|
||||
@@ -550,26 +585,36 @@
|
||||
<el-button
|
||||
v-if="agr.status === 'dsx'"
|
||||
class="btn-op-item primary"
|
||||
:loading="agreementOperatingId === agr.id && agreementOperatingAction === 'activate'"
|
||||
:disabled="!!agreementOperatingId"
|
||||
@click="handleActivateAgreement(agr)"
|
||||
>
|
||||
<el-icon><Check /></el-icon>生效
|
||||
<el-icon v-if="!(agreementOperatingId === agr.id && agreementOperatingAction === 'activate')"><Check /></el-icon>生效
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="agr.status === 'dsx' || agr.status === 'sxz'"
|
||||
class="btn-op-item"
|
||||
:loading="agreementOperatingId === agr.id && agreementOperatingAction === 'void'"
|
||||
:disabled="!!agreementOperatingId"
|
||||
@click="handleVoidAgreement(agr)"
|
||||
>
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
<el-icon v-if="!(agreementOperatingId === agr.id && agreementOperatingAction === 'void')"><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:class="{ primary: agr.status !== 'dsx' }"
|
||||
:disabled="!!agreementOperatingId"
|
||||
@click="handleEditAgreement(agr)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDeleteAgreement(agr)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:loading="agreementOperatingId === agr.id && agreementOperatingAction === 'delete'"
|
||||
:disabled="!!agreementOperatingId"
|
||||
@click="handleDeleteAgreement(agr)"
|
||||
>
|
||||
<el-icon v-if="!(agreementOperatingId === agr.id && agreementOperatingAction === 'delete')"><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -632,6 +677,8 @@
|
||||
class="list-body list-wrap protocol-price-list-wrap"
|
||||
>
|
||||
<el-table
|
||||
v-loading="isAgreementPriceTableLoading(agr.id)"
|
||||
element-loading-text="删除中…"
|
||||
:data="agreementPricesMap[agr.id] || []"
|
||||
class="list-table protocol-card-price-table"
|
||||
stripe
|
||||
@@ -727,8 +774,14 @@
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row: priceRow }">
|
||||
<el-button class="btn-op-item" @click="handleDeletePriceInline(priceRow, agr)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:loading="deletingAgreementPriceId === priceRow.id"
|
||||
:disabled="!!deletingAgreementPriceId"
|
||||
@click="handleDeletePriceInline(priceRow, agr)"
|
||||
>
|
||||
<el-icon v-if="deletingAgreementPriceId !== priceRow.id"><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -861,23 +914,22 @@
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form v-if="priceSelectorMode === 'project'" label-position="top" class="price-selector-filter">
|
||||
<el-form-item label="选择项目价格体系">
|
||||
<el-select
|
||||
v-model="selectedProjectSystemId"
|
||||
placeholder="请选择项目价格体系"
|
||||
style="width: 100%"
|
||||
@change="handleProjectSystemChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="sys in projectSystemList"
|
||||
:key="sys.id"
|
||||
:label="sys.systemName"
|
||||
:value="sys.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-if="priceSelectorMode === 'project'" class="price-selector-filter price-selector-system-row">
|
||||
<span class="price-selector-system-label">选择项目价格体系</span>
|
||||
<el-select
|
||||
v-model="selectedProjectSystemId"
|
||||
class="price-selector-system-select"
|
||||
placeholder="请选择项目价格体系"
|
||||
@change="handleProjectSystemChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="sys in projectSystemList"
|
||||
:key="sys.id"
|
||||
:label="sys.systemName"
|
||||
:value="sys.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="price-selector-filter">
|
||||
<el-input
|
||||
v-model="priceSelectorKeyword"
|
||||
@@ -896,12 +948,17 @@
|
||||
v-loading="priceSelectorLoading"
|
||||
:data="filteredPriceSelectorList"
|
||||
border
|
||||
size="small"
|
||||
class="list-table"
|
||||
max-height="480"
|
||||
@selection-change="handlePriceSelectorChange"
|
||||
>
|
||||
<el-table-column type="selection" width="45" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="45"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:selectable="priceSelectorRowSelectable"
|
||||
/>
|
||||
<el-table-column prop="projectName" label="项目名称" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="projectCode" label="项目编码" min-width="100" align="center" header-align="center" />
|
||||
<el-table-column prop="cellTypeName" label="细胞类型" min-width="100" align="center" header-align="center" />
|
||||
@@ -921,10 +978,11 @@
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:disabled="priceSelectorSelected.length === 0"
|
||||
:loading="priceConfirmLoading"
|
||||
:disabled="priceSelectorSelected.length === 0 || priceConfirmLoading"
|
||||
@click="handleConfirmPriceSelector"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
<el-icon v-if="!priceConfirmLoading"><Check /></el-icon>
|
||||
确认添加({{ priceSelectorSelected.length }})
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -988,6 +1046,7 @@
|
||||
import ChannelViewDialog from './ChannelViewDialog.vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import {
|
||||
fetchChannelList,
|
||||
fetchCreateChannel,
|
||||
@@ -1119,6 +1178,25 @@
|
||||
}>
|
||||
}
|
||||
|
||||
function resolveChannelAttachments(row: any) {
|
||||
if (Array.isArray(row?.attachmentFiles) && row.attachmentFiles.length) {
|
||||
return row.attachmentFiles.map((a: any, i: number) => ({
|
||||
id: a.id || `att_${i}`,
|
||||
fileName: a.fileName || '',
|
||||
fileUrl: a.fileUrl || '',
|
||||
fileSize: a.fileSize ?? 0,
|
||||
fileType: a.fileType || ''
|
||||
}))
|
||||
}
|
||||
if (Array.isArray(row?.attachments) && row.attachments.length) {
|
||||
return row.attachments.map((a: any) => ({ ...a }))
|
||||
}
|
||||
if (Array.isArray(row?.attachmentList) && row.attachmentList.length) {
|
||||
return row.attachmentList.map((a: any) => ({ ...a }))
|
||||
}
|
||||
return protocolUrlsToAttachments(String(row?.protocolAttachments || ''))
|
||||
}
|
||||
|
||||
const agreementTypeTagMap: Record<string, { label: string; type: string }> = {
|
||||
kyxy: { label: '框架协议', type: 'primary' },
|
||||
zsxy: { label: '中试协议', type: 'success' },
|
||||
@@ -1248,6 +1326,11 @@
|
||||
const agreementFormStatus = ref('dsx')
|
||||
const agreementSubmitLoading = ref(false)
|
||||
const agreementActivateLoading = ref(false)
|
||||
const agreementOperatingId = ref<string | null>(null)
|
||||
const agreementOperatingAction = ref<'activate' | 'void' | 'delete' | null>(null)
|
||||
const deletingInvoiceId = ref<string | null>(null)
|
||||
const deletingMailId = ref<string | null>(null)
|
||||
const priceConfirmLoading = ref(false)
|
||||
const agreementFormRef = ref()
|
||||
const agreementFormData = reactive({
|
||||
agreementType: '' as string,
|
||||
@@ -1278,6 +1361,48 @@
|
||||
const priceSelectorTitle = ref('从价格体系添加')
|
||||
const priceSelectorKeyword = ref('')
|
||||
const priceSelectorLoading = ref(false)
|
||||
const deletingAgreementPriceId = ref<string | null>(null)
|
||||
const deletingAgreementPriceAgreementId = ref<string | null>(null)
|
||||
|
||||
function isAgreementPriceTableLoading(agreementId: string) {
|
||||
return (
|
||||
deletingAgreementPriceAgreementId.value === agreementId &&
|
||||
deletingAgreementPriceId.value != null
|
||||
)
|
||||
}
|
||||
|
||||
function removeAgreementPriceFromMap(agreementId: string, priceId: string) {
|
||||
const list = agreementPricesMap.value[agreementId] || []
|
||||
agreementPricesMap.value = {
|
||||
...agreementPricesMap.value,
|
||||
[agreementId]: list.filter((item) => item.id !== priceId)
|
||||
}
|
||||
}
|
||||
|
||||
/** 协议项目信息唯一键:项目编码 + 细胞类型 + 规格 */
|
||||
function buildAgreementPriceItemKey(item: Record<string, any>) {
|
||||
const code = String(item.projectCode || item.projectName || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
const cellType = String(item.cellTypeCode || item.cellTypeName || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
const spec = String(item.specText || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
return `${code}|${cellType}|${spec}`
|
||||
}
|
||||
|
||||
function getAgreementPriceKeySet(agreementId: string | null) {
|
||||
if (!agreementId) return new Set<string>()
|
||||
const list = agreementPricesMap.value[agreementId] || []
|
||||
return new Set(list.map((item: any) => buildAgreementPriceItemKey(item)))
|
||||
}
|
||||
|
||||
function priceSelectorRowSelectable(row: any) {
|
||||
if (!currentAgreementId.value) return true
|
||||
return !getAgreementPriceKeySet(currentAgreementId.value).has(buildAgreementPriceItemKey(row))
|
||||
}
|
||||
|
||||
const filteredPriceSelectorList = computed(() => {
|
||||
const kw = priceSelectorKeyword.value.trim().toLowerCase()
|
||||
@@ -1380,13 +1505,7 @@
|
||||
formData.contactName = row.contactName || ''
|
||||
formData.contactPhone = row.contactPhone || ''
|
||||
formData.contactEmail = row.contactEmail || ''
|
||||
if (Array.isArray(row.attachments) && row.attachments.length) {
|
||||
formData.attachments = row.attachments.map((a: any) => ({ ...a }))
|
||||
} else if (Array.isArray(row.attachmentList) && row.attachmentList.length) {
|
||||
formData.attachments = row.attachmentList.map((a: any) => ({ ...a }))
|
||||
} else {
|
||||
formData.attachments = protocolUrlsToAttachments(String(row.protocolAttachments || ''))
|
||||
}
|
||||
formData.attachments = resolveChannelAttachments(row)
|
||||
formData.remark = row.remark || ''
|
||||
activeTab.value = 'basic'
|
||||
dialogVisible.value = true
|
||||
@@ -1468,10 +1587,16 @@
|
||||
// 主表删除
|
||||
// ---------------------------------------------------------------------------
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm('确定要删除该渠道吗?关联的子表数据也将清除。', '提示', { type: 'warning' })
|
||||
await fetchDeleteChannel(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该渠道吗?关联的子表数据也将清除。', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteChannel(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
@@ -1580,15 +1705,25 @@
|
||||
}
|
||||
|
||||
async function handleDeleteInvoice(row: any) {
|
||||
await ElMessageBox.confirm('确定删除该开票信息吗?', '提示', { type: 'warning' })
|
||||
if (editingId.value) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除该开票信息吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!editingId.value) {
|
||||
const idx = invoiceList.value.findIndex((r: any) => r.id === row.id)
|
||||
if (idx !== -1) invoiceList.value.splice(idx, 1)
|
||||
return
|
||||
}
|
||||
deletingInvoiceId.value = row.id
|
||||
try {
|
||||
await fetchDeleteChannelInvoice(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadSubTables(editingId.value)
|
||||
} else {
|
||||
// 本地模式:从本地数组中移除
|
||||
const idx = invoiceList.value.findIndex((r: any) => r.id === row.id)
|
||||
if (idx !== -1) invoiceList.value.splice(idx, 1)
|
||||
} catch {
|
||||
/* 全局已提示 */
|
||||
} finally {
|
||||
deletingInvoiceId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1663,14 +1798,25 @@
|
||||
}
|
||||
|
||||
async function handleDeleteMail(row: any) {
|
||||
await ElMessageBox.confirm('确定删除该邮寄地址吗?', '提示', { type: 'warning' })
|
||||
if (editingId.value) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除该邮寄地址吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!editingId.value) {
|
||||
const idx = mailList.value.findIndex((r: any) => r.id === row.id)
|
||||
if (idx !== -1) mailList.value.splice(idx, 1)
|
||||
return
|
||||
}
|
||||
deletingMailId.value = row.id
|
||||
try {
|
||||
await fetchDeleteChannelMailAddress(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadSubTables(editingId.value)
|
||||
} else {
|
||||
const idx = mailList.value.findIndex((r: any) => r.id === row.id)
|
||||
if (idx !== -1) mailList.value.splice(idx, 1)
|
||||
} catch {
|
||||
/* 全局已提示 */
|
||||
} finally {
|
||||
deletingMailId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1786,17 +1932,43 @@
|
||||
}
|
||||
|
||||
async function handleDeleteAgreement(row: any) {
|
||||
await ElMessageBox.confirm('确定删除该协议吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteChannelAgreement(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadAgreementsForDialog(agreementChannelId.value!)
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除该协议吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
agreementOperatingId.value = row.id
|
||||
agreementOperatingAction.value = 'delete'
|
||||
try {
|
||||
await fetchDeleteChannelAgreement(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadAgreementsForDialog(agreementChannelId.value!)
|
||||
} catch {
|
||||
/* 全局已提示 */
|
||||
} finally {
|
||||
agreementOperatingId.value = null
|
||||
agreementOperatingAction.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function handleActivateAgreement(row: any) {
|
||||
await ElMessageBox.confirm('确定将该协议设为生效中吗?', '协议生效', { type: 'info' })
|
||||
await fetchActivateChannelAgreement(row.id)
|
||||
ElMessage.success('生效成功')
|
||||
await loadAgreementsForDialog(agreementChannelId.value!)
|
||||
try {
|
||||
await ElMessageBox.confirm('确定将该协议设为生效中吗?', '协议生效', { type: 'info' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
agreementOperatingId.value = row.id
|
||||
agreementOperatingAction.value = 'activate'
|
||||
try {
|
||||
await fetchActivateChannelAgreement(row.id)
|
||||
ElMessage.success('生效成功')
|
||||
await loadAgreementsForDialog(agreementChannelId.value!)
|
||||
} catch {
|
||||
/* 全局已提示 */
|
||||
} finally {
|
||||
agreementOperatingId.value = null
|
||||
agreementOperatingAction.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function handleActivateAgreementFromForm() {
|
||||
@@ -1825,10 +1997,23 @@
|
||||
}
|
||||
|
||||
async function handleVoidAgreement(row: any) {
|
||||
await ElMessageBox.confirm('确定作废该协议吗?作废后不可恢复。', '提示', { type: 'warning' })
|
||||
await fetchVoidChannelAgreement(row.id)
|
||||
ElMessage.success('操作成功')
|
||||
await loadAgreementsForDialog(agreementChannelId.value!)
|
||||
try {
|
||||
await ElMessageBox.confirm('确定作废该协议吗?作废后不可恢复。', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
agreementOperatingId.value = row.id
|
||||
agreementOperatingAction.value = 'void'
|
||||
try {
|
||||
await fetchVoidChannelAgreement(row.id)
|
||||
ElMessage.success('操作成功')
|
||||
await loadAgreementsForDialog(agreementChannelId.value!)
|
||||
} catch {
|
||||
/* 全局已提示 */
|
||||
} finally {
|
||||
agreementOperatingId.value = null
|
||||
agreementOperatingAction.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -1867,10 +2052,25 @@
|
||||
}
|
||||
|
||||
async function handleDeletePriceInline(priceRow: any, agreement: any) {
|
||||
await ElMessageBox.confirm('确定删除该价格明细吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteChannelAgreementPrice(priceRow.id)
|
||||
ElMessage.success('删除成功')
|
||||
await refreshAgreementPrices(agreement.id)
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除该价格明细吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const agreementId = agreement.id
|
||||
const priceId = priceRow.id
|
||||
deletingAgreementPriceId.value = priceId
|
||||
deletingAgreementPriceAgreementId.value = agreementId
|
||||
try {
|
||||
await fetchDeleteChannelAgreementPrice(priceId)
|
||||
removeAgreementPriceFromMap(agreementId, priceId)
|
||||
ElMessage.success('删除成功')
|
||||
} catch {
|
||||
/* 全局已提示 */
|
||||
} finally {
|
||||
deletingAgreementPriceId.value = null
|
||||
deletingAgreementPriceAgreementId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddPriceCommand(command: string) {
|
||||
@@ -1881,7 +2081,6 @@
|
||||
} else if (command === 'fromProjectSystem') {
|
||||
priceSelectorMode.value = 'project'
|
||||
priceSelectorTitle.value = '从项目价格体系添加'
|
||||
selectedProjectSystemId.value = ''
|
||||
openProjectPriceSelector()
|
||||
}
|
||||
}
|
||||
@@ -1918,6 +2117,11 @@
|
||||
try {
|
||||
const res = await fetchProjectSystemList({ page: 1, pageSize: 100 })
|
||||
projectSystemList.value = (res as any)?.list || []
|
||||
const first = projectSystemList.value[0]
|
||||
if (first?.id) {
|
||||
selectedProjectSystemId.value = first.id
|
||||
await handleProjectSystemChange(first.id)
|
||||
}
|
||||
} catch {
|
||||
projectSystemList.value = []
|
||||
}
|
||||
@@ -1953,37 +2157,85 @@
|
||||
|
||||
async function handleConfirmPriceSelector() {
|
||||
if (!currentAgreementId.value) return
|
||||
const existingList = agreementPricesMap.value[currentAgreementId.value] || []
|
||||
const existingIds = new Set(existingList.map((item: any) => String(item.sourcePriceId || '')).filter(Boolean))
|
||||
const existingKeys = getAgreementPriceKeySet(currentAgreementId.value)
|
||||
const batchKeys = new Set<string>()
|
||||
const toAdd: any[] = []
|
||||
let skippedCount = 0
|
||||
|
||||
for (const row of priceSelectorSelected.value) {
|
||||
if (existingIds.has(row.id)) continue
|
||||
try {
|
||||
await fetchCreateChannelAgreementPrice(currentAgreementId.value, {
|
||||
projectName: row.projectName || '',
|
||||
projectCode: row.projectCode || '',
|
||||
cellTypeCode: row.cellTypeCode || undefined,
|
||||
cellTypeName: row.cellTypeName || '',
|
||||
specText: row.specText || '',
|
||||
originalPrice: row.unitPrice ?? null,
|
||||
agreementPrice: null,
|
||||
remark: row.remark || undefined
|
||||
})
|
||||
} catch {
|
||||
/* */
|
||||
const key = buildAgreementPriceItemKey(row)
|
||||
if (existingKeys.has(key) || batchKeys.has(key)) {
|
||||
skippedCount += 1
|
||||
continue
|
||||
}
|
||||
batchKeys.add(key)
|
||||
toAdd.push(row)
|
||||
}
|
||||
priceSelectorVisible.value = false
|
||||
if (currentAgreementId.value) {
|
||||
await refreshAgreementPrices(currentAgreementId.value)
|
||||
|
||||
if (!toAdd.length) {
|
||||
ElMessage.warning(
|
||||
skippedCount > 0 ? '所选项目均已存在,不可重复添加' : '请选择要添加的项目'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
priceConfirmLoading.value = true
|
||||
let successCount = 0
|
||||
try {
|
||||
for (const row of toAdd) {
|
||||
try {
|
||||
await fetchCreateChannelAgreementPrice(currentAgreementId.value, {
|
||||
projectName: row.projectName || '',
|
||||
projectCode: row.projectCode || '',
|
||||
cellTypeCode: row.cellTypeCode || undefined,
|
||||
cellTypeName: row.cellTypeName || '',
|
||||
specText: row.specText || '',
|
||||
originalPrice: row.unitPrice ?? null,
|
||||
agreementPrice: null,
|
||||
remark: row.remark || undefined
|
||||
})
|
||||
successCount += 1
|
||||
} catch {
|
||||
/* */
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
ElMessage.success(`成功添加 ${successCount} 条项目`)
|
||||
}
|
||||
if (skippedCount > 0) {
|
||||
ElMessage.warning(`已跳过 ${skippedCount} 条重复项目`)
|
||||
}
|
||||
priceSelectorVisible.value = false
|
||||
if (currentAgreementId.value) {
|
||||
await refreshAgreementPrices(currentAgreementId.value)
|
||||
}
|
||||
} finally {
|
||||
priceConfirmLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeletePrice(row: any) {
|
||||
await ElMessageBox.confirm('确定删除该价格明细吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteChannelAgreementPrice(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
if (currentAgreementId.value) {
|
||||
await refreshAgreementPrices(currentAgreementId.value)
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除该价格明细吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const agreementId = currentAgreementId.value
|
||||
if (!agreementId) return
|
||||
const priceId = row.id
|
||||
deletingAgreementPriceId.value = priceId
|
||||
deletingAgreementPriceAgreementId.value = agreementId
|
||||
try {
|
||||
await fetchDeleteChannelAgreementPrice(priceId)
|
||||
removeAgreementPriceFromMap(agreementId, priceId)
|
||||
priceList.value = priceList.value.filter((item) => item.id !== priceId)
|
||||
ElMessage.success('删除成功')
|
||||
} catch {
|
||||
/* 全局已提示 */
|
||||
} finally {
|
||||
deletingAgreementPriceId.value = null
|
||||
deletingAgreementPriceAgreementId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2083,15 +2335,7 @@
|
||||
}
|
||||
|
||||
function handleDownloadAttachment(file: any) {
|
||||
const url = fullFileUrl(file.fileUrl)
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = file.fileName || ''
|
||||
a.target = '_blank'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2648,6 +2892,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.price-selector-system-row {
|
||||
align-items: center;
|
||||
|
||||
.price-selector-system-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text, #334155);
|
||||
line-height: 36px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.price-selector-system-select {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.form-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
@@ -227,6 +227,7 @@
|
||||
fetchDeleteCheckupInstitutionPackage,
|
||||
fetchCheckupInstitutionProjects
|
||||
} from '@/api/admin'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
|
||||
const props = defineProps<{
|
||||
institutionId: string
|
||||
@@ -374,10 +375,16 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm(`确定删除套餐「${row.packageName}」?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteCheckupInstitutionPackage(row.id)
|
||||
ElMessage.success('已删除')
|
||||
await loadPackages()
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除套餐「${row.packageName}」?`, '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteCheckupInstitutionPackage(row.id)
|
||||
ElMessage.success('已删除')
|
||||
await loadPackages()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function openProjectSelector() {
|
||||
|
||||
@@ -332,6 +332,7 @@
|
||||
fetchUpdateCheckupInstitutionProject,
|
||||
fetchDeleteCheckupInstitutionProject
|
||||
} from '@/api/admin'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
|
||||
defineOptions({ name: 'PilotCheckupInstitution' })
|
||||
|
||||
@@ -555,12 +556,18 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm(`确定删除「${row.institutionName}」?其下属项目价格将一并删除。`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await fetchDeleteCheckupInstitution(row.id)
|
||||
ElMessage.success('已删除')
|
||||
loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除「${row.institutionName}」?其下属项目价格将一并删除。`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteCheckupInstitution(row.id)
|
||||
ElMessage.success('已删除')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -693,10 +700,16 @@
|
||||
}
|
||||
|
||||
async function handleDeleteItem(row: any) {
|
||||
await ElMessageBox.confirm(`确定删除「${row.projectName}」?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteCheckupInstitutionProject(row.id)
|
||||
ElMessage.success('已删除')
|
||||
loadItems()
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除「${row.projectName}」?`, '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteCheckupInstitutionProject(row.id)
|
||||
ElMessage.success('已删除')
|
||||
await loadItems()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
|
||||
@@ -523,10 +523,16 @@
|
||||
|
||||
// --- Delete ---
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm('确定要删除该体系吗?删除后关联的价格明细也将清除。', '提示', { type: 'warning' })
|
||||
await fetchDeleteProjectSystem(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该体系吗?删除后关联的价格明细也将清除。', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteProjectSystem(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
// --- Batch delete ---
|
||||
|
||||
@@ -61,6 +61,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
|
||||
@@ -125,8 +126,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
|
||||
<el-table-column label="操作" width="320" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
@@ -145,11 +149,90 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看弹窗 -->
|
||||
<el-dialog
|
||||
v-model="viewVisible"
|
||||
title="查看耗材包"
|
||||
width="960px"
|
||||
class="dialog-form-layout pack-view-dialog"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-loading="viewLoading" class="pack-view-body">
|
||||
<template v-if="viewDetail">
|
||||
<div class="form-section">
|
||||
<div class="section-title">基本信息</div>
|
||||
<div class="pack-view-kv-grid">
|
||||
<div class="pack-view-kv">
|
||||
<span class="pack-view-label">编号</span>
|
||||
<span class="pack-view-value">{{ viewDetail.packCode || '—' }}</span>
|
||||
</div>
|
||||
<div class="pack-view-kv">
|
||||
<span class="pack-view-label">名称</span>
|
||||
<span class="pack-view-value">{{ viewDetail.packName || '—' }}</span>
|
||||
</div>
|
||||
<div class="pack-view-kv">
|
||||
<span class="pack-view-label">使用场景</span>
|
||||
<span class="pack-view-value">{{ viewDetail.usageScene || '—' }}</span>
|
||||
</div>
|
||||
<div class="pack-view-kv">
|
||||
<span class="pack-view-label">类型</span>
|
||||
<span class="pack-view-value">{{ packTypeLabel(viewDetail.packType) }}</span>
|
||||
</div>
|
||||
<div class="pack-view-kv">
|
||||
<span class="pack-view-label">状态</span>
|
||||
<span class="pack-view-value">
|
||||
<el-tag :type="viewDetail.status === 1 ? 'success' : 'danger'" size="small">
|
||||
{{ viewDetail.status === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="pack-view-kv">
|
||||
<span class="pack-view-label">备注</span>
|
||||
<span class="pack-view-value">{{ viewDetail.remark || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<div class="section-title">耗材明细</div>
|
||||
<el-table :data="viewDetail.items || []" stripe class="list-table inner-table">
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="56"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => index + 1"
|
||||
/>
|
||||
<el-table-column prop="consumableCode" label="耗材编码" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="consumableName" label="耗材名称" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="specModel" label="规格型号" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="standardUsage" label="标准用量" min-width="100" align="center" header-align="center" />
|
||||
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="140" align="center" header-align="center" />
|
||||
</el-table>
|
||||
<el-empty
|
||||
v-if="!(viewDetail.items || []).length"
|
||||
description="暂无耗材明细"
|
||||
:image-size="48"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="viewVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="800px"
|
||||
width="960px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
@@ -167,7 +250,7 @@
|
||||
<!-- 基本信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">基本信息</div>
|
||||
<div class="form-grid-2">
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="编号" prop="packCode">
|
||||
<el-input v-model="formData.packCode" placeholder="请输入编号" />
|
||||
</el-form-item>
|
||||
@@ -201,16 +284,33 @@
|
||||
添加耗材
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="formData.items" stripe size="small" class="inner-table">
|
||||
<el-table :data="formData.items" stripe class="inner-table">
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="56"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => index + 1"
|
||||
/>
|
||||
<el-table-column prop="consumableCode" label="耗材编码" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="consumableName" label="耗材名称" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="specModel" label="规格型号" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column label="标准用量" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.standardUsage" placeholder="用量" size="small" />
|
||||
<el-input
|
||||
v-model="row.standardUsage"
|
||||
placeholder="用量"
|
||||
class="inner-cell-input inner-cell-input--usage"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
|
||||
<el-table-column label="备注" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" placeholder="备注" class="inner-cell-input" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column label="操作" width="80" align="center" header-align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" size="small" @click="formData.items.splice($index, 1)">
|
||||
@@ -244,29 +344,42 @@
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-input
|
||||
v-model="consumableKeyword"
|
||||
placeholder="搜索耗材编码/名称"
|
||||
clearable
|
||||
style="margin-bottom: 12px"
|
||||
@input="filterConsumables"
|
||||
/>
|
||||
<div class="consumable-select-toolbar">
|
||||
<el-input
|
||||
v-model="consumableKeyword"
|
||||
placeholder="搜索耗材编码/名称"
|
||||
clearable
|
||||
class="consumable-select-search"
|
||||
@input="filterConsumables"
|
||||
/>
|
||||
<span class="consumable-select-count">
|
||||
已选 <span class="sel-num">{{ selectedConsumables.length }}</span> 项
|
||||
</span>
|
||||
</div>
|
||||
<el-table
|
||||
ref="consumableSelectTableRef"
|
||||
class="list-table consumable-select-table"
|
||||
:data="filteredConsumableList"
|
||||
stripe
|
||||
size="small"
|
||||
height="360"
|
||||
highlight-current-row
|
||||
@current-change="handleConsumableSelect"
|
||||
row-key="id"
|
||||
@selection-change="handleConsumableSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" header-align="center" :reserve-selection="true" />
|
||||
<el-table-column prop="consumableCode" label="耗材编码" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="consumableName" label="耗材名称" min-width="140" align="center" header-align="center" />
|
||||
<el-table-column prop="specModel" label="规格型号" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="unit" label="单位" min-width="80" align="center" header-align="center" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="selectConsumableVisible = false">取消</el-button>
|
||||
<el-button class="btn-main" type="primary" :disabled="!selectedConsumable" @click="confirmAddConsumable">
|
||||
确定
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:disabled="selectedConsumables.length === 0"
|
||||
@click="confirmAddConsumable"
|
||||
>
|
||||
确定添加
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -275,7 +388,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import { Plus, Close, Check, View } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchConsumablePackList,
|
||||
fetchConsumablePackDetail,
|
||||
@@ -284,6 +397,7 @@
|
||||
fetchDeleteConsumablePack,
|
||||
fetchAllConsumables
|
||||
} from '@/api/production'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
|
||||
defineOptions({ name: 'ProductionConsumablePack' })
|
||||
|
||||
@@ -298,6 +412,15 @@
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const editingId = ref<string | null>(null)
|
||||
const viewVisible = ref(false)
|
||||
const viewLoading = ref(false)
|
||||
const viewDetail = ref<any>(null)
|
||||
|
||||
function packTypeLabel(packType?: string) {
|
||||
if (packType === 'bzhcb') return '标准耗材包'
|
||||
if (packType === 'zdyhcb') return '自定义耗材包'
|
||||
return packType || '—'
|
||||
}
|
||||
|
||||
const formData = reactive({
|
||||
packCode: '',
|
||||
@@ -320,7 +443,8 @@
|
||||
const allConsumableList = ref<any[]>([])
|
||||
const filteredConsumableList = ref<any[]>([])
|
||||
const consumableKeyword = ref('')
|
||||
const selectedConsumable = ref<any>(null)
|
||||
const consumableSelectTableRef = ref()
|
||||
const selectedConsumables = ref<any[]>([])
|
||||
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectedRows.value = rows
|
||||
@@ -371,13 +495,28 @@
|
||||
nextTick(() => formRef.value?.clearValidate?.())
|
||||
}
|
||||
|
||||
async function handleView(row: any) {
|
||||
viewVisible.value = true
|
||||
viewLoading.value = true
|
||||
viewDetail.value = null
|
||||
try {
|
||||
viewDetail.value = await fetchConsumablePackDetail(row.id)
|
||||
} catch {
|
||||
ElMessage.error('加载详情失败')
|
||||
viewVisible.value = false
|
||||
} finally {
|
||||
viewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit(row: any) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑耗材包'
|
||||
editingId.value = row.id
|
||||
const packId = row.id
|
||||
resetForm()
|
||||
editingId.value = packId
|
||||
try {
|
||||
const detail = await fetchConsumablePackDetail(row.id)
|
||||
const detail = await fetchConsumablePackDetail(packId)
|
||||
formData.packCode = detail.packCode || ''
|
||||
formData.packName = detail.packName || ''
|
||||
formData.usageScene = detail.usageScene || ''
|
||||
@@ -409,6 +548,7 @@
|
||||
consumableName: item.consumableName,
|
||||
specModel: item.specModel,
|
||||
standardUsage: item.standardUsage,
|
||||
unit: item.unit || undefined,
|
||||
remark: item.remark || ''
|
||||
}))
|
||||
}
|
||||
@@ -428,27 +568,39 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm('确定要删除该耗材包吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteConsumablePack(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该耗材包吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteConsumablePack(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个耗材包吗?`, '提示', { type: 'warning' })
|
||||
for (const id of ids) {
|
||||
await fetchDeleteConsumablePack(id)
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个耗材包吗?`, '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
ElMessage.success('批量删除成功')
|
||||
selectedRows.value = []
|
||||
await loadData()
|
||||
await withFullscreenLoading(async () => {
|
||||
for (const id of ids) {
|
||||
await fetchDeleteConsumablePack(id)
|
||||
}
|
||||
ElMessage.success('批量删除成功')
|
||||
selectedRows.value = []
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.batchDelete)
|
||||
}
|
||||
|
||||
// 选择耗材弹窗
|
||||
async function openAddConsumableDialog() {
|
||||
selectedConsumable.value = null
|
||||
selectedConsumables.value = []
|
||||
consumableKeyword.value = ''
|
||||
if (allConsumableList.value.length === 0) {
|
||||
try {
|
||||
@@ -459,6 +611,7 @@
|
||||
}
|
||||
filteredConsumableList.value = [...allConsumableList.value]
|
||||
selectConsumableVisible.value = true
|
||||
nextTick(() => consumableSelectTableRef.value?.clearSelection?.())
|
||||
}
|
||||
|
||||
function filterConsumables() {
|
||||
@@ -474,30 +627,41 @@
|
||||
)
|
||||
}
|
||||
|
||||
function handleConsumableSelect(row: any) {
|
||||
selectedConsumable.value = row
|
||||
function handleConsumableSelectionChange(rows: any[]) {
|
||||
selectedConsumables.value = rows
|
||||
}
|
||||
|
||||
function confirmAddConsumable() {
|
||||
if (!selectedConsumable.value) return
|
||||
const c = selectedConsumable.value
|
||||
// 检查是否已添加
|
||||
const exists = formData.items.some(
|
||||
(item) => item.consumableId === c.id || item.consumableCode === c.consumableCode
|
||||
)
|
||||
if (exists) {
|
||||
ElMessage.warning('该耗材已在明细中')
|
||||
return
|
||||
if (!selectedConsumables.value.length) return
|
||||
let added = 0
|
||||
let skipped = 0
|
||||
for (const c of selectedConsumables.value) {
|
||||
const exists = formData.items.some(
|
||||
(item) => item.consumableId === c.id || item.consumableCode === c.consumableCode
|
||||
)
|
||||
if (exists) {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
formData.items.push({
|
||||
consumableId: c.id,
|
||||
consumableCode: c.consumableCode || '',
|
||||
consumableName: c.consumableName || '',
|
||||
specModel: c.specModel || '',
|
||||
standardUsage: c.standardUsage || '',
|
||||
unit: c.unit || '',
|
||||
remark: ''
|
||||
})
|
||||
added++
|
||||
}
|
||||
if (added > 0) {
|
||||
ElMessage.success(
|
||||
skipped > 0 ? `已添加 ${added} 项,${skipped} 项重复已跳过` : `已添加 ${added} 项耗材`
|
||||
)
|
||||
selectConsumableVisible.value = false
|
||||
} else {
|
||||
ElMessage.warning('所选耗材均已在明细中')
|
||||
}
|
||||
formData.items.push({
|
||||
consumableId: c.id,
|
||||
consumableCode: c.consumableCode || '',
|
||||
consumableName: c.consumableName || '',
|
||||
specModel: c.specModel || '',
|
||||
standardUsage: '',
|
||||
remark: ''
|
||||
})
|
||||
selectConsumableVisible.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -533,5 +697,80 @@
|
||||
|
||||
.inner-table {
|
||||
width: 100%;
|
||||
|
||||
:deep(td.el-table__cell) {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(.inner-cell-input) {
|
||||
.el-input__wrapper {
|
||||
min-height: 36px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.inner-cell-input--usage .el-input__inner) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.consumable-select-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.consumable-select-search {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.consumable-select-count {
|
||||
flex-shrink: 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
white-space: nowrap;
|
||||
|
||||
.sel-num {
|
||||
color: var(--primary, #10b981);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.pack-view-body {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.pack-view-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px 24px;
|
||||
}
|
||||
|
||||
.pack-view-kv {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pack-view-label {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.pack-view-value {
|
||||
font-size: 14px;
|
||||
color: var(--mint-text, #1e293b);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>耗材管理</h1>
|
||||
<p class="page-desc">维护生产耗材的编码、名称、规格及标准用量等信息。</p>
|
||||
<p class="page-desc">维护生产耗材的编码、名称、规格、标准用量及单位等信息。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
@@ -72,6 +72,7 @@
|
||||
<el-table-column prop="consumableName" label="耗材名称" min-width="150" align="center" header-align="center" />
|
||||
<el-table-column prop="specModel" label="规格型号" min-width="140" align="center" header-align="center" />
|
||||
<el-table-column prop="standardUsage" label="标准用量" min-width="110" align="center" header-align="center" />
|
||||
<el-table-column prop="unit" label="单位" min-width="90" align="center" header-align="center" />
|
||||
<el-table-column label="描述" min-width="160" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ truncateText(row.description, 20) }}
|
||||
@@ -132,6 +133,16 @@
|
||||
<el-form-item label="标准用量">
|
||||
<el-input v-model="formData.standardUsage" placeholder="请输入标准用量" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位">
|
||||
<el-select v-model="formData.unit" placeholder="请选择单位" clearable filterable>
|
||||
<el-option
|
||||
v-for="item in unitOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" class="form-col-span-2">
|
||||
<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
@@ -175,9 +186,12 @@
|
||||
fetchUpdateConsumable,
|
||||
fetchDeleteConsumable
|
||||
} from '@/api/production'
|
||||
import { fetchItemUnitList } from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'ProductionConsumable' })
|
||||
|
||||
const unitOptions = ref<{ label: string; value: string }[]>([])
|
||||
|
||||
function truncateText(text: string, max: number) {
|
||||
if (!text) return ''
|
||||
return text.length > max ? text.slice(0, max) + '…' : text
|
||||
@@ -202,6 +216,7 @@
|
||||
consumableName: '',
|
||||
specModel: '',
|
||||
standardUsage: '',
|
||||
unit: '',
|
||||
description: '',
|
||||
remark: ''
|
||||
})
|
||||
@@ -226,6 +241,7 @@
|
||||
{ label: '耗材名称', value: row.consumableName },
|
||||
{ label: '规格型号', value: row.specModel },
|
||||
{ label: '标准用量', value: row.standardUsage },
|
||||
{ label: '单位', value: row.unit },
|
||||
{ label: '描述', value: row.description, span: true },
|
||||
{ label: '备注', value: row.remark, span: true }
|
||||
]
|
||||
@@ -264,6 +280,7 @@
|
||||
formData.consumableName = ''
|
||||
formData.specModel = ''
|
||||
formData.standardUsage = ''
|
||||
formData.unit = ''
|
||||
formData.description = ''
|
||||
formData.remark = ''
|
||||
editingId.value = null
|
||||
@@ -285,6 +302,7 @@
|
||||
formData.consumableName = row.consumableName || ''
|
||||
formData.specModel = row.specModel || ''
|
||||
formData.standardUsage = row.standardUsage ?? ''
|
||||
formData.unit = row.unit || ''
|
||||
formData.description = row.description || ''
|
||||
formData.remark = row.remark || ''
|
||||
dialogVisible.value = true
|
||||
@@ -300,6 +318,7 @@
|
||||
consumableName: formData.consumableName.trim(),
|
||||
specModel: formData.specModel?.trim() || undefined,
|
||||
standardUsage: formData.standardUsage ?? undefined,
|
||||
unit: formData.unit?.trim() || undefined,
|
||||
description: formData.description?.trim() || undefined,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
@@ -324,10 +343,16 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm('确定要删除该耗材吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteConsumable(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该耗材吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteConsumable(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
@@ -344,8 +369,10 @@
|
||||
}, fullscreenLoadingText.batchDelete)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
loadData()
|
||||
const res = await fetchItemUnitList({ page: 1, pageSize: 100 })
|
||||
unitOptions.value = (res.list || []).map((u: any) => ({ label: u.unitName, value: u.unitName }))
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -288,10 +288,16 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm('确定要删除该类型吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteOutputType(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该类型吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteOutputType(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
|
||||
@@ -288,10 +288,16 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm('确定要删除该分类吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteProcessCategory(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该分类吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteProcessCategory(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -343,10 +343,16 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm('确定要删除该试剂吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteReagent(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该试剂吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteReagent(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
|
||||
@@ -129,29 +129,75 @@
|
||||
<el-input v-model="formData.versionNo" placeholder="请输入版本号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<div class="upload-card-row">
|
||||
<div
|
||||
v-for="(url, idx) in attachmentList"
|
||||
:key="idx"
|
||||
class="upload-card-item"
|
||||
>
|
||||
<el-icon class="upload-card-icon"><Document /></el-icon>
|
||||
<span class="upload-card-name" :title="getFileName(url)">{{ getFileName(url) }}</span>
|
||||
<el-icon class="upload-card-remove" @click="removeAttachment(idx)"><Close /></el-icon>
|
||||
</div>
|
||||
<el-upload
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
@change="handleFileChange"
|
||||
accept="*"
|
||||
>
|
||||
<div class="upload-card-trigger">
|
||||
<el-icon><Plus /></el-icon>
|
||||
<span>上传</span>
|
||||
<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>
|
||||
</el-upload>
|
||||
</div>
|
||||
<span class="upload-hint"
|
||||
>支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB</span
|
||||
>
|
||||
</div>
|
||||
<div class="upload-card-hint">支持上传多个附件文件。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" placeholder="可选" />
|
||||
@@ -170,21 +216,104 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
title="文件预览"
|
||||
width="800px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="previewType === 'image'" class="preview-container">
|
||||
<img
|
||||
:src="previewUrl"
|
||||
alt="预览"
|
||||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="preview-container">
|
||||
<video
|
||||
:src="previewUrl"
|
||||
controls
|
||||
style="max-width: 100%; max-height: 70vh; display: block; margin: 0 auto"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewRow?.sopName || 'SOP详情'"
|
||||
:icon="ScaleToOriginal"
|
||||
:meta="viewRow?.createTime || ''"
|
||||
:sections="viewSections"
|
||||
/>
|
||||
>
|
||||
<template #append>
|
||||
<div v-if="viewRow" class="view-section sop-view-attachment-section">
|
||||
<h3>附件</h3>
|
||||
<div v-if="viewAttachments.length" class="attachment-list">
|
||||
<div
|
||||
v-for="(file, index) in viewAttachments"
|
||||
:key="file.id || index"
|
||||
class="attachment-item attachment-item--view"
|
||||
>
|
||||
<div
|
||||
class="attachment-preview"
|
||||
@click="
|
||||
isImageFile(file) || isVideoFile(file)
|
||||
? handlePreviewAttachment(file)
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<template v-if="isImageFile(file)">
|
||||
<img :src="fullFileUrl(file.fileUrl)" :alt="file.fileName" />
|
||||
</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.stop="handleDownloadAttachment(file)"
|
||||
>
|
||||
<Download />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="sop-view-attachment-empty">暂无附件</p>
|
||||
</div>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted, computed, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check, View, ScaleToOriginal, Document } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Plus,
|
||||
Close,
|
||||
Check,
|
||||
View,
|
||||
ScaleToOriginal,
|
||||
Document,
|
||||
Download,
|
||||
Delete,
|
||||
VideoPlay,
|
||||
Loading
|
||||
} from '@element-plus/icons-vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import {
|
||||
@@ -194,9 +323,18 @@
|
||||
fetchDeleteSop
|
||||
} from '@/api/production'
|
||||
import { fetchUploadAttachment } from '@/api/business'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
|
||||
defineOptions({ name: 'ProductionSop' })
|
||||
|
||||
type SopAttachment = {
|
||||
id: string
|
||||
fileName: string
|
||||
fileUrl: string
|
||||
fileType?: string
|
||||
_uploading?: boolean
|
||||
}
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<any[]>([])
|
||||
const selectedRows = ref<any[]>([])
|
||||
@@ -210,18 +348,19 @@
|
||||
const editingId = ref<string | null>(null)
|
||||
const viewVisible = ref(false)
|
||||
const viewRow = ref<any>(null)
|
||||
const attachmentUploadRef = ref()
|
||||
const previewVisible = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewType = ref<'image' | 'video'>('image')
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
const formData = reactive({
|
||||
sopCode: '',
|
||||
sopName: '',
|
||||
versionNo: '',
|
||||
sopAttachments: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const attachmentList = computed(() => {
|
||||
if (!formData.sopAttachments) return []
|
||||
return formData.sopAttachments.split(',').filter(Boolean)
|
||||
remark: '',
|
||||
attachments: [] as SopAttachment[]
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
@@ -229,27 +368,89 @@
|
||||
sopName: [{ required: true, message: '请输入SOP文档名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
function fullFileUrl(suffix: string) {
|
||||
if (!suffix) return ''
|
||||
if (suffix.startsWith('http://') || suffix.startsWith('https://') || suffix.startsWith('/'))
|
||||
return suffix
|
||||
return `${minioBaseUrl}/${suffix}`
|
||||
}
|
||||
|
||||
function getFileName(url: string) {
|
||||
if (!url) return ''
|
||||
const parts = url.split('/')
|
||||
return decodeURIComponent(parts[parts.length - 1] || '')
|
||||
}
|
||||
|
||||
function attachmentsFromUrls(urls: string): SopAttachment[] {
|
||||
if (!urls) return []
|
||||
return urls
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
.map((url, i) => ({
|
||||
id: `existing_${i}`,
|
||||
fileName: getFileName(url),
|
||||
fileUrl: url,
|
||||
fileType: ''
|
||||
}))
|
||||
}
|
||||
|
||||
function attachmentsFromApiFiles(files: any[]): SopAttachment[] {
|
||||
if (!files?.length) return []
|
||||
return files.map((f, i) => ({
|
||||
id: f.id || `existing_${i}`,
|
||||
fileName: f.fileName || getFileName(f.fileUrl || ''),
|
||||
fileUrl: f.fileUrl || '',
|
||||
fileType: f.fileType || ''
|
||||
}))
|
||||
}
|
||||
|
||||
function resolveAttachments(row: any): SopAttachment[] {
|
||||
if (row?.attachmentFiles?.length) {
|
||||
return attachmentsFromApiFiles(row.attachmentFiles)
|
||||
}
|
||||
return attachmentsFromUrls(row?.sopAttachments || '')
|
||||
}
|
||||
|
||||
function syncSopAttachmentsFromList() {
|
||||
formData.sopAttachments = formData.attachments
|
||||
.filter((a) => a.fileUrl && !a._uploading)
|
||||
.map((a) => a.fileUrl)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
function isImageFile(file: SopAttachment) {
|
||||
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: SopAttachment) {
|
||||
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/')
|
||||
)
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
const viewAttachments = computed(() => {
|
||||
if (!viewRow.value) return []
|
||||
return resolveAttachments(viewRow.value)
|
||||
})
|
||||
|
||||
const viewSections = computed(() => {
|
||||
if (!viewRow.value) return []
|
||||
const row = viewRow.value
|
||||
const attachCount = row.sopAttachments ? row.sopAttachments.split(',').filter(Boolean).length : 0
|
||||
const attachLinks = row.sopAttachments
|
||||
? row.sopAttachments
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
.map((url: string) => getFileName(url))
|
||||
.join('、')
|
||||
: '无'
|
||||
return [
|
||||
{
|
||||
title: '基本信息',
|
||||
@@ -258,16 +459,33 @@
|
||||
{ label: 'SOP文档名称', value: row.sopName },
|
||||
{ label: '版本号', value: row.versionNo },
|
||||
{ label: '附件数', value: String(attachCount) },
|
||||
{ label: '附件列表', value: attachLinks, span: true },
|
||||
{ label: '备注', value: row.remark, span: true }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function handleFileChange(uploadFile: any) {
|
||||
const file = uploadFile?.raw
|
||||
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: '',
|
||||
fileType: file.type || '',
|
||||
_uploading: true
|
||||
})
|
||||
const idx = formData.attachments.length - 1
|
||||
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('business_type', 'sop')
|
||||
@@ -275,19 +493,40 @@
|
||||
fd.append('file', file)
|
||||
const res: any = await fetchUploadAttachment(fd)
|
||||
if (res?.fileUrl) {
|
||||
const current = formData.sopAttachments ? formData.sopAttachments.split(',').filter(Boolean) : []
|
||||
current.push(res.fileUrl)
|
||||
formData.sopAttachments = current.join(',')
|
||||
formData.attachments[idx] = {
|
||||
...formData.attachments[idx],
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
fileType: res.fileType || file.type || '',
|
||||
_uploading: false
|
||||
}
|
||||
syncSopAttachmentsFromList()
|
||||
} else {
|
||||
formData.attachments.splice(idx, 1)
|
||||
}
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
formData.attachments.splice(idx, 1)
|
||||
} finally {
|
||||
nextTick(() => attachmentUploadRef.value?.clearFiles())
|
||||
}
|
||||
}
|
||||
|
||||
function removeAttachment(idx: number) {
|
||||
const current = formData.sopAttachments.split(',').filter(Boolean)
|
||||
current.splice(idx, 1)
|
||||
formData.sopAttachments = current.join(',')
|
||||
function removeAttachment(index: number) {
|
||||
formData.attachments.splice(index, 1)
|
||||
syncSopAttachmentsFromList()
|
||||
}
|
||||
|
||||
function handlePreviewAttachment(file: SopAttachment) {
|
||||
if (file._uploading) return
|
||||
previewUrl.value = fullFileUrl(file.fileUrl)
|
||||
previewType.value = isVideoFile(file) ? 'video' : 'image'
|
||||
previewVisible.value = true
|
||||
}
|
||||
|
||||
function handleDownloadAttachment(file: SopAttachment) {
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
@@ -322,6 +561,7 @@
|
||||
formData.versionNo = ''
|
||||
formData.sopAttachments = ''
|
||||
formData.remark = ''
|
||||
formData.attachments = []
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
@@ -341,6 +581,7 @@
|
||||
formData.sopName = row.sopName || ''
|
||||
formData.versionNo = row.versionNo || ''
|
||||
formData.sopAttachments = row.sopAttachments || ''
|
||||
formData.attachments = resolveAttachments(row)
|
||||
formData.remark = row.remark || ''
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
@@ -348,6 +589,7 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate?.()
|
||||
syncSopAttachmentsFromList()
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
@@ -378,10 +620,16 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
await ElMessageBox.confirm('确定要删除该SOP文档吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteSop(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该SOP文档吗?', '提示', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteSop(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}, fullscreenLoadingText.delete)
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
@@ -410,83 +658,208 @@
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
.attachment-upload {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.upload-card-item {
|
||||
width: 120px;
|
||||
height: 96px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.upload-card-icon {
|
||||
font-size: 24px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.upload-card-name {
|
||||
font-size: 12px;
|
||||
color: #374151;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-card-remove {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
font-size: 14px;
|
||||
color: #ef4444;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.upload-card-trigger {
|
||||
width: 120px;
|
||||
.upload-card {
|
||||
width: 140px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
transition: border-color 0.2s;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
border-style: solid;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-hint {
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
color: #10b981;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.upload-hint {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 140px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
&.is-uploading {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
width: 100%;
|
||||
height: 96px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
overflow: hidden;
|
||||
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;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.sop-view-attachment-section {
|
||||
padding: 18px 22px;
|
||||
border-bottom: none;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 14px;
|
||||
padding-left: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
border-left: 3px solid #10b981;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.sop-view-attachment-empty {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.attachment-item--view {
|
||||
.attachment-preview {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.attachment-preview:has(img),
|
||||
.attachment-preview:has(.attachment-preview__icon--video) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -148,7 +148,21 @@
|
||||
:icon="Tickets"
|
||||
:meta="viewDetail?.taskNo || ''"
|
||||
:sections="viewSections"
|
||||
/>
|
||||
>
|
||||
<template #append>
|
||||
<div
|
||||
v-if="viewDetail?.attachmentFiles?.length"
|
||||
class="view-section qc-task-view-attachments"
|
||||
>
|
||||
<h3>检测材料</h3>
|
||||
<ProtocolAttachmentReadonly
|
||||
:files="viewDetail.attachmentFiles"
|
||||
@preview="handleViewAttachmentPreview"
|
||||
@download="handleViewAttachmentDownload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -158,6 +172,7 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close, Check, Search, RefreshLeft, VideoPlay, Upload, View, Tickets, EditPen } from '@element-plus/icons-vue'
|
||||
import ProtocolFinanceConfirmFields from '@/components/business/protocol-finance-confirm-fields/index.vue'
|
||||
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import {
|
||||
@@ -175,6 +190,7 @@
|
||||
} from '@/api/qc-task'
|
||||
import { fetchQcTaskResults, fetchCompleteTaskWithResult, type QcTaskResultItem } from '@/api/qc-task-result'
|
||||
import { taskTypeLabel, taskStatusLabel } from '@/utils/qc-labels'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
|
||||
defineOptions({ name: 'QualityInspectionQcTask' })
|
||||
|
||||
@@ -199,6 +215,35 @@
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewDetail = ref<QcTaskDetail | null>(null)
|
||||
const minioBaseUrl = (import.meta.env.VITE_MINIO_BASE_URL || '').replace(/\/$/, '')
|
||||
|
||||
function apiFilesToFinanceItems(
|
||||
files?: Array<{ id?: string; fileUrl: string; fileName?: string; fileType?: string }>
|
||||
): FinanceAttachmentItem[] {
|
||||
if (!files?.length) return []
|
||||
return files.map((f, i) => ({
|
||||
id: f.id || `att_${i}`,
|
||||
fileName: f.fileName || '',
|
||||
fileUrl: f.fileUrl || '',
|
||||
fileType: f.fileType || ''
|
||||
}))
|
||||
}
|
||||
|
||||
function attachmentFullUrl(url: string) {
|
||||
const u = String(url || '').trim()
|
||||
if (!u) return ''
|
||||
if (/^https?:\/\//i.test(u)) return u
|
||||
return minioBaseUrl ? `${minioBaseUrl}/${u.replace(/^\//, '')}` : u
|
||||
}
|
||||
|
||||
function handleViewAttachmentPreview(file: Record<string, unknown>) {
|
||||
const url = attachmentFullUrl(String(file.fileUrl || ''))
|
||||
if (url) window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function handleViewAttachmentDownload(file: Record<string, unknown>) {
|
||||
downloadAttachmentFile(file, { fullFileUrl: attachmentFullUrl })
|
||||
}
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
@@ -272,11 +317,19 @@
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openAttach(row: QcTask) {
|
||||
async function openAttach(row: QcTask) {
|
||||
attachTaskId.value = row.id
|
||||
attachRemark.value = ''
|
||||
attachments.value = []
|
||||
attachRemark.value = row.remark || ''
|
||||
attachments.value = apiFilesToFinanceItems(row.attachmentFiles)
|
||||
attachVisible.value = true
|
||||
if (attachments.value.length) return
|
||||
try {
|
||||
const detail = await fetchQcTaskDetail(row.id)
|
||||
attachRemark.value = detail.remark || ''
|
||||
attachments.value = apiFilesToFinanceItems(detail.attachmentFiles)
|
||||
} catch {
|
||||
/* 列表未带附件时拉详情 */
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAttachments() {
|
||||
@@ -313,10 +366,15 @@
|
||||
async function openResult(row: QcTask) {
|
||||
resultTaskId.value = row.id
|
||||
resultTaskType.value = row.taskType
|
||||
resultRemark.value = ''
|
||||
resultAttachments.value = []
|
||||
resultRemark.value = row.remark || ''
|
||||
resultAttachments.value = apiFilesToFinanceItems(row.attachmentFiles)
|
||||
resultLoading.value = true
|
||||
try {
|
||||
if (row.taskType === 'dsfjc' && !resultAttachments.value.length) {
|
||||
const detail = await fetchQcTaskDetail(row.id)
|
||||
resultRemark.value = detail.remark || ''
|
||||
resultAttachments.value = apiFilesToFinanceItems(detail.attachmentFiles)
|
||||
}
|
||||
const results = await fetchQcTaskResults(row.id)
|
||||
if (results && results.length > 0) {
|
||||
resultParams.value = results
|
||||
@@ -379,4 +437,19 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.qc-task-view-attachments {
|
||||
padding: 18px 22px;
|
||||
border-bottom: none;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 14px;
|
||||
padding-left: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
border-left: 3px solid #10b981;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -493,6 +493,7 @@
|
||||
type FinanceAttachmentItem
|
||||
} from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
|
||||
defineOptions({ name: 'QualityInspectionSampleReception' })
|
||||
|
||||
@@ -620,14 +621,7 @@
|
||||
}
|
||||
|
||||
function handleAttachmentDownload(file: Record<string, unknown>) {
|
||||
const url = String(file.fileUrl || '')
|
||||
if (!url) return
|
||||
const a = document.createElement('a')
|
||||
a.href = fullFileUrl(url)
|
||||
a.download = String(file.fileName || 'download')
|
||||
a.target = '_blank'
|
||||
a.rel = 'noopener'
|
||||
a.click()
|
||||
downloadAttachmentFile(file, { fullFileUrl })
|
||||
}
|
||||
|
||||
function getCurrentUserHandler() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 捐赠协议单审核 -->
|
||||
<!-- 捐赠协议审核 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
@@ -9,24 +9,27 @@
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">捐赠协议审核</h2>
|
||||
<h2 class="title-bar-heading">捐赠协议单列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="捐赠编号/母亲姓名" clearable />
|
||||
<el-input v-model="searchForm.keyword" placeholder="捐赠编号/备注" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="审核状态">
|
||||
<el-form-item label="协议状态">
|
||||
<el-select
|
||||
v-model="searchForm.auditStatus"
|
||||
v-model="searchForm.donationStatus"
|
||||
clearable
|
||||
placeholder="全部"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="审核通过" value="shtg" />
|
||||
<el-option label="审核驳回" value="shth" />
|
||||
<el-option label="待生效" value="dsx" />
|
||||
<el-option label="生效中" value="sxz" />
|
||||
<el-option label="已过期" value="ygq" />
|
||||
<el-option label="已作废" value="yzf" />
|
||||
<el-option label="转细胞存储" value="zckc" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -49,19 +52,45 @@
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="handleBatchAudit('shtg')">
|
||||
<el-button type="primary" class="btn-main" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="openAuditDialog()">
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
批量审核通过
|
||||
审核
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="handleBatchAudit('shth')">
|
||||
<el-icon><CircleClose /></el-icon>
|
||||
批量审核驳回
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="openStatusChangeDialog('activate')"
|
||||
>
|
||||
协议生效
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="handleBatchStatusChange('activate')">
|
||||
批量生效
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="openStatusChangeDialog('expire')"
|
||||
>
|
||||
协议过期
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="handleBatchStatusChange('void')">
|
||||
批量作废
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="openStatusChangeDialog('void')"
|
||||
>
|
||||
协议作废
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="!selectedRows.length"
|
||||
@click="openStatusChangeDialog('toStorage')"
|
||||
>
|
||||
转细胞存储
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,6 +121,24 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="donationStatus"
|
||||
label="状态"
|
||||
width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{
|
||||
donationStatusLabel(String(row.donationStatus || ''))
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="donationDate"
|
||||
label="捐赠日期"
|
||||
width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="motherName"
|
||||
label="母亲姓名"
|
||||
@@ -100,39 +147,100 @@
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="auditStatus"
|
||||
label="审核状态"
|
||||
width="120"
|
||||
prop="motherPhone"
|
||||
label="母亲电话"
|
||||
min-width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="motherIdCard"
|
||||
label="母亲身份证号"
|
||||
min-width="190"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="fatherName"
|
||||
label="父亲姓名"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="newbornSex"
|
||||
label="新生儿性别"
|
||||
width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="row.auditStatus === 'shtg' ? 'success' : row.auditStatus === 'shth' ? 'danger' : 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ auditStatusLabel(String(row.auditStatus || '')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #default="{ row }">{{
|
||||
newbornSexLabel(String(row.newbornSex || ''))
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="auditRemark"
|
||||
label="审核备注"
|
||||
prop="deliveryMode"
|
||||
label="分娩方式"
|
||||
width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{
|
||||
deliveryModeLabel(String(row.deliveryMode || ''))
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornWeightGram"
|
||||
label="新生儿体重(克)"
|
||||
width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="regionName"
|
||||
label="采集地区"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="hospitalName"
|
||||
label="采集医院"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="collectorUserName"
|
||||
label="采集专员"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="salesUserName"
|
||||
label="销售人员"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sampleCollectTime"
|
||||
label="样本采集时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="auditTime"
|
||||
label="审核时间"
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="260"
|
||||
min-width="380"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
@@ -144,6 +252,12 @@
|
||||
<el-button class="btn-op-item" @click="openAuditDialog(row)"
|
||||
><el-icon><CircleCheck /></el-icon>审核</el-button
|
||||
>
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)"
|
||||
><el-icon><Edit /></el-icon>编辑</el-button
|
||||
>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)"
|
||||
><el-icon><Delete /></el-icon>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -160,6 +274,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<DonationFormDialog
|
||||
v-model="formDialogVisible"
|
||||
:edit-id="formDialogEditId"
|
||||
:list-row="formDialogRow"
|
||||
@saved="onFormSaved"
|
||||
/>
|
||||
|
||||
<!-- 审核弹窗 -->
|
||||
<DonationAuditDialog
|
||||
v-model="auditDialogVisible"
|
||||
:targets="auditTargets"
|
||||
@submitted="onAuditSubmitted"
|
||||
/>
|
||||
|
||||
<!-- 状态变更弹窗(生效/过期/作废/转细胞存储) -->
|
||||
<DonationStatusChangeDialog
|
||||
v-model="statusChangeDialogVisible"
|
||||
:action-type="statusChangeAction"
|
||||
:targets="statusChangeTargets"
|
||||
@submitted="onStatusChangeSubmitted"
|
||||
/>
|
||||
|
||||
<!-- 查看弹窗 -->
|
||||
<DonationViewDialog
|
||||
v-model="viewVisible"
|
||||
@@ -167,14 +304,6 @@
|
||||
:list-row="viewRow"
|
||||
show-audit-history
|
||||
/>
|
||||
|
||||
<!-- 审核弹窗 -->
|
||||
<DonationAuditDialog
|
||||
v-model="auditDialogVisible"
|
||||
:donation-id="auditDonationId"
|
||||
:list-row="auditRow"
|
||||
@saved="onAuditSubmitted"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -182,55 +311,76 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
View,
|
||||
Edit,
|
||||
Delete,
|
||||
Search,
|
||||
RefreshLeft,
|
||||
CircleCheck,
|
||||
CircleClose
|
||||
CircleCheck
|
||||
} from '@element-plus/icons-vue'
|
||||
import DonationFormDialog from '../list/DonationFormDialog.vue'
|
||||
import DonationViewDialog from '../list/DonationViewDialog.vue'
|
||||
import DonationAuditDialog from '../list/DonationAuditDialog.vue'
|
||||
import DonationAuditDialog, { type DonationAuditTarget } from '../list/DonationAuditDialog.vue'
|
||||
import DonationStatusChangeDialog, {
|
||||
type DonationStatusChangeAction,
|
||||
type DonationStatusChangeTarget
|
||||
} from '../list/DonationStatusChangeDialog.vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import {
|
||||
fetchResearchDonationAuditList,
|
||||
fetchSubmitDonationAudit,
|
||||
fetchDonationStatusChange,
|
||||
fetchResearchDonationList,
|
||||
fetchDeleteResearchDonation,
|
||||
type ResearchDonationRow
|
||||
} from '@/api/research-donation'
|
||||
|
||||
defineOptions({ name: 'ResearchDonationAudit' })
|
||||
|
||||
/* ====== 标签映射 ====== */
|
||||
function auditStatusLabel(s: string) {
|
||||
if (s === 'shtg') return '审核通过'
|
||||
if (s === 'shth') return '审核驳回'
|
||||
return s || '—'
|
||||
function donationStatusLabel(s: string) {
|
||||
const m: Record<string, string> = {
|
||||
dsx: '待生效',
|
||||
sxz: '生效中',
|
||||
ygq: '已过期',
|
||||
yzf: '已作废',
|
||||
zckc: '转细胞存储'
|
||||
}
|
||||
return m[s] || s || '-'
|
||||
}
|
||||
|
||||
function newbornSexLabel(s: string) {
|
||||
if (s === '1') return '男'
|
||||
if (s === '0') return '女'
|
||||
return s || '-'
|
||||
}
|
||||
|
||||
function deliveryModeLabel(s: string) {
|
||||
if (s === 'sc') return '顺产'
|
||||
if (s === 'pgc') return '剖宫产'
|
||||
return s || '-'
|
||||
}
|
||||
|
||||
/* ====== 列表 ====== */
|
||||
type AuditRow = Record<string, unknown>
|
||||
|
||||
const searchForm = reactive<{ keyword: string; auditStatus: string }>({
|
||||
const searchForm = reactive<{ keyword: string; donationStatus: string }>({
|
||||
keyword: '',
|
||||
auditStatus: ''
|
||||
donationStatus: ''
|
||||
})
|
||||
const tableData = ref<AuditRow[]>([])
|
||||
const selectedRows = ref<AuditRow[]>([])
|
||||
const tableData = ref<ResearchDonationRow[]>([])
|
||||
const selectedRows = ref<ResearchDonationRow[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
const handleSelectionChange = (rows: AuditRow[]) => {
|
||||
const handleSelectionChange = (rows: ResearchDonationRow[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchResearchDonationAuditList({
|
||||
const res = await fetchResearchDonationList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
auditStatus: searchForm.auditStatus || undefined
|
||||
donationStatus: searchForm.donationStatus || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
@@ -246,7 +396,7 @@
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.auditStatus = ''
|
||||
searchForm.donationStatus = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
@@ -254,22 +404,49 @@
|
||||
/* ====== 查看弹窗 ====== */
|
||||
const viewVisible = ref(false)
|
||||
const viewDonationId = ref<string | null>(null)
|
||||
const viewRow = ref<AuditRow | null>(null)
|
||||
const viewRow = ref<ResearchDonationRow | null>(null)
|
||||
|
||||
function handleView(row: AuditRow) {
|
||||
function handleView(row: ResearchDonationRow) {
|
||||
viewDonationId.value = String(row.id)
|
||||
viewRow.value = row
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
/* ====== 新增/编辑弹窗 ====== */
|
||||
const formDialogVisible = ref(false)
|
||||
const formDialogEditId = ref<string | null>(null)
|
||||
const formDialogRow = ref<ResearchDonationRow | null>(null)
|
||||
|
||||
function handleAdd() {
|
||||
formDialogEditId.value = null
|
||||
formDialogRow.value = null
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row: ResearchDonationRow) {
|
||||
formDialogEditId.value = row.id
|
||||
formDialogRow.value = row
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
function onFormSaved() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
/* ====== 审核 ====== */
|
||||
const auditDialogVisible = ref(false)
|
||||
const auditDonationId = ref<string | null>(null)
|
||||
const auditRow = ref<AuditRow | null>(null)
|
||||
const auditTargets = ref<DonationAuditTarget[]>([])
|
||||
|
||||
function openAuditDialog(row: AuditRow) {
|
||||
auditDonationId.value = String(row.id)
|
||||
auditRow.value = row
|
||||
function openAuditDialog(row?: ResearchDonationRow) {
|
||||
const rows = row ? [row] : selectedRows.value
|
||||
if (!rows.length) {
|
||||
ElMessage.warning('请先选择要审核的协议单')
|
||||
return
|
||||
}
|
||||
auditTargets.value = rows.map((r) => ({
|
||||
id: String(r.id),
|
||||
donationNo: r.donationNo != null ? String(r.donationNo) : undefined
|
||||
}))
|
||||
auditDialogVisible.value = true
|
||||
}
|
||||
|
||||
@@ -278,59 +455,60 @@
|
||||
loadData()
|
||||
}
|
||||
|
||||
/* ====== 批量审核 ====== */
|
||||
async function handleBatchAudit(status: string) {
|
||||
if (!selectedRows.value.length) {
|
||||
ElMessage.warning('请先选择要操作的记录')
|
||||
/* ====== 状态变更(生效/过期/作废/转细胞存储) ====== */
|
||||
const statusChangeDialogVisible = ref(false)
|
||||
const statusChangeAction = ref<DonationStatusChangeAction>('activate')
|
||||
const statusChangeTargets = ref<DonationStatusChangeTarget[]>([])
|
||||
|
||||
function openStatusChangeDialog(action: DonationStatusChangeAction, row?: ResearchDonationRow) {
|
||||
const rows = row ? [row] : selectedRows.value
|
||||
if (!rows.length) {
|
||||
ElMessage.warning(`请先选择要操作的协议单`)
|
||||
return
|
||||
}
|
||||
const label = status === 'shtg' ? '审核通过' : '审核驳回'
|
||||
await ElMessageBox.confirm(
|
||||
`确认将选中的 ${selectedRows.value.length} 条记录标记为「${label}」?`,
|
||||
'批量审核',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
const rows = [...selectedRows.value]
|
||||
await withFullscreenLoading(async () => {
|
||||
for (const row of rows) {
|
||||
await fetchSubmitDonationAudit(String(row.id), { auditStatus: status })
|
||||
}
|
||||
ElMessage.success(`批量${label}成功`)
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
}, fullscreenLoadingText.batchAudit)
|
||||
statusChangeAction.value = action
|
||||
statusChangeTargets.value = rows.map((r) => ({
|
||||
id: String(r.id),
|
||||
donationNo: r.donationNo != null ? String(r.donationNo) : undefined
|
||||
}))
|
||||
statusChangeDialogVisible.value = true
|
||||
}
|
||||
|
||||
/* ====== 批量状态变更 ====== */
|
||||
async function handleBatchStatusChange(action: 'activate' | 'void') {
|
||||
if (!selectedRows.value.length) {
|
||||
ElMessage.warning('请先选择要操作的记录')
|
||||
return
|
||||
}
|
||||
const label = action === 'activate' ? '生效' : '作废'
|
||||
const targetStatus = action === 'activate' ? 'sxz' : 'yzf'
|
||||
await ElMessageBox.confirm(
|
||||
`确认将选中的 ${selectedRows.value.length} 条记录标记为「${label}」?`,
|
||||
`批量${label}`,
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
function onStatusChangeSubmitted() {
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
}
|
||||
|
||||
/* ====== 删除 ====== */
|
||||
function handleDelete(row: ResearchDonationRow) {
|
||||
ElMessageBox.confirm(`确认删除捐赠编号「${row.donationNo}」?`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
await fetchDeleteResearchDonation(String(row.id))
|
||||
loadData()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
if (!selectedRows.value.length) return
|
||||
await ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录?`, '批量删除', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const rows = [...selectedRows.value]
|
||||
await withFullscreenLoading(async () => {
|
||||
for (const row of rows) {
|
||||
await fetchDonationStatusChange(String(row.id), { donationStatus: targetStatus })
|
||||
await fetchDeleteResearchDonation(String(row.id))
|
||||
}
|
||||
ElMessage.success(`批量${label}成功`)
|
||||
ElMessage.success('批量删除成功')
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
}, fullscreenLoadingText.save)
|
||||
}, fullscreenLoadingText.batchDelete)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- 捐赠协议单 — 审核弹窗 -->
|
||||
<!-- 捐赠协议单 — 提交审核(支持批量,写入 biz_donation_audit) -->
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
@@ -8,12 +8,15 @@
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
style="backdrop-filter: none"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<p class="audit-target-hint">
|
||||
捐赠编号:<strong>{{ listRow?.donationNo || '—' }}</strong>
|
||||
<p v-if="targets.length > 1" class="audit-target-hint">
|
||||
将对已选
|
||||
<strong>{{ targets.length }}</strong> 条捐赠协议单提交相同审核结果,每条各记一条审核记录。
|
||||
</p>
|
||||
<p v-else-if="targets.length === 1" class="audit-target-hint">
|
||||
捐赠编号:<strong>{{ targets[0].donationNo || '—' }}</strong>
|
||||
</p>
|
||||
|
||||
<el-form
|
||||
@@ -27,7 +30,7 @@
|
||||
<el-form-item label="审核状态" prop="auditStatus">
|
||||
<el-radio-group v-model="form.auditStatus">
|
||||
<el-radio value="shtg">审核通过</el-radio>
|
||||
<el-radio value="shth">审核驳回</el-radio>
|
||||
<el-radio value="shth">审核退回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核备注">
|
||||
@@ -57,21 +60,26 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import { fetchSubmitDonationAudit } from '@/api/research-donation'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
|
||||
export type DonationAuditTarget = {
|
||||
id: string
|
||||
donationNo?: string
|
||||
}
|
||||
|
||||
defineOptions({ name: 'DonationAuditDialog' })
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
donationId: string | null
|
||||
listRow?: Record<string, unknown> | null
|
||||
targets: DonationAuditTarget[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', val: boolean): void
|
||||
(e: 'saved'): void
|
||||
(e: 'submitted'): void
|
||||
}>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
@@ -85,7 +93,9 @@
|
||||
auditStatus: [{ required: true, message: '请选择审核状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const dialogTitle = computed(() => '捐赠协议单审核')
|
||||
const dialogTitle = computed(() =>
|
||||
props.targets.length > 1 ? `批量审核(${props.targets.length} 条)` : '捐赠协议单审核'
|
||||
)
|
||||
|
||||
function resetForm() {
|
||||
form.auditStatus = 'shtg'
|
||||
@@ -95,22 +105,44 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid || !props.donationId) return
|
||||
if (!valid || !props.targets.length) return
|
||||
|
||||
const payload = {
|
||||
auditStatus: form.auditStatus,
|
||||
auditRemark: form.auditRemark.trim() || undefined
|
||||
}
|
||||
const rows = [...props.targets]
|
||||
const isBatch = rows.length > 1
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await withFullscreenLoading(
|
||||
async () => {
|
||||
await fetchSubmitDonationAudit(props.donationId!, payload)
|
||||
emit('update:modelValue', false)
|
||||
emit('saved')
|
||||
let ok = 0
|
||||
const failed: string[] = []
|
||||
for (const row of rows) {
|
||||
try {
|
||||
await fetchSubmitDonationAudit(String(row.id), payload, {
|
||||
showSuccessMessage: !isBatch
|
||||
})
|
||||
ok += 1
|
||||
} catch {
|
||||
failed.push(row.donationNo || String(row.id))
|
||||
}
|
||||
}
|
||||
if (isBatch) {
|
||||
if (failed.length) {
|
||||
ElMessage.warning(`成功 ${ok} 条,失败 ${failed.length} 条`)
|
||||
} else {
|
||||
ElMessage.success(`批量审核成功,共 ${ok} 条`)
|
||||
}
|
||||
}
|
||||
if (ok > 0) {
|
||||
emit('update:modelValue', false)
|
||||
emit('submitted')
|
||||
}
|
||||
},
|
||||
fullscreenLoadingText.save
|
||||
isBatch ? fullscreenLoadingText.batchAudit : fullscreenLoadingText.save
|
||||
)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -8,12 +8,16 @@
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
style="backdrop-filter: none"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<p class="status-change-hint">
|
||||
捐赠编号:<strong>{{ listRow?.donationNo || '—' }}</strong>
|
||||
<p v-if="targets.length > 1" class="status-change-hint">
|
||||
将对已选
|
||||
<strong>{{ targets.length }}</strong> 条捐赠协议单执行<strong>{{ actionLabel }}</strong
|
||||
>操作,每条各记一次变更。
|
||||
</p>
|
||||
<p v-else-if="targets.length === 1" class="status-change-hint">
|
||||
捐赠编号:<strong>{{ targets[0].donationNo || '—' }}</strong>
|
||||
</p>
|
||||
|
||||
<el-form
|
||||
@@ -24,27 +28,6 @@
|
||||
class="biz-dialog-form"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item label="变更操作" prop="operationType">
|
||||
<el-radio-group v-model="form.operationType" @change="handleOperationChange">
|
||||
<el-radio
|
||||
v-for="op in availableOperations"
|
||||
:key="op.value"
|
||||
:value="op.value"
|
||||
>
|
||||
{{ op.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-alert
|
||||
v-if="form.operationType === 'zckc'"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="convert-alert"
|
||||
>
|
||||
转为细胞存储后将创建新的细胞存储协议,需补填缴费信息。
|
||||
</el-alert>
|
||||
|
||||
<el-form-item :label="timeLabel" prop="changeTime">
|
||||
<el-date-picker
|
||||
v-model="form.changeTime"
|
||||
@@ -54,7 +37,7 @@
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-form-item :label="descLabel">
|
||||
<el-input
|
||||
v-model="form.changeDesc"
|
||||
type="textarea"
|
||||
@@ -81,102 +64,97 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import { fetchDonationStatusChange } from '@/api/research-donation'
|
||||
import { fetchDonationStatusChange, fetchConvertToStorage } from '@/api/research-donation'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
|
||||
type OperationType = 'activate' | 'expire' | 'void' | 'zckc'
|
||||
export type DonationStatusChangeAction = 'activate' | 'expire' | 'void' | 'toStorage'
|
||||
export type DonationStatusChangeTarget = {
|
||||
id: string
|
||||
donationNo?: string
|
||||
}
|
||||
|
||||
defineOptions({ name: 'DonationStatusChangeDialog' })
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
donationId: string | null
|
||||
listRow?: Record<string, unknown> | null
|
||||
actionType: DonationStatusChangeAction
|
||||
targets: DonationStatusChangeTarget[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', val: boolean): void
|
||||
(e: 'saved'): void
|
||||
(e: 'submitted'): void
|
||||
}>()
|
||||
|
||||
const OPERATION_CONFIG: Record<
|
||||
OperationType,
|
||||
const ACTION_CONFIG: Record<
|
||||
DonationStatusChangeAction,
|
||||
{
|
||||
label: string
|
||||
timeLabel: string
|
||||
targetStatus: string
|
||||
descLabel: string
|
||||
status: string
|
||||
timeKey: string
|
||||
descKey: string
|
||||
}
|
||||
> = {
|
||||
activate: {
|
||||
label: '生效',
|
||||
timeLabel: '生效时间',
|
||||
targetStatus: 'sxz'
|
||||
label: '协议生效',
|
||||
timeLabel: '协议生效时间',
|
||||
descLabel: '协议生效描述',
|
||||
status: 'sxz',
|
||||
timeKey: 'effectiveTime',
|
||||
descKey: 'effectiveDesc'
|
||||
},
|
||||
expire: {
|
||||
label: '过期',
|
||||
timeLabel: '过期时间',
|
||||
targetStatus: 'ygq'
|
||||
label: '协议过期',
|
||||
timeLabel: '协议过期时间',
|
||||
descLabel: '协议过期描述',
|
||||
status: 'ygq',
|
||||
timeKey: 'expireTime',
|
||||
descKey: 'expireDesc'
|
||||
},
|
||||
void: {
|
||||
label: '作废',
|
||||
timeLabel: '作废时间',
|
||||
targetStatus: 'yzf'
|
||||
label: '协议作废',
|
||||
timeLabel: '协议作废时间',
|
||||
descLabel: '协议作废描述',
|
||||
status: 'yzf',
|
||||
timeKey: 'voidTime',
|
||||
descKey: 'voidDesc'
|
||||
},
|
||||
zckc: {
|
||||
toStorage: {
|
||||
label: '转细胞存储',
|
||||
timeLabel: '转换时间',
|
||||
targetStatus: 'zckc'
|
||||
timeLabel: '转细胞存储时间',
|
||||
descLabel: '转细胞存储描述',
|
||||
status: 'zckc',
|
||||
timeKey: 'convertTime',
|
||||
descKey: 'convertDesc'
|
||||
}
|
||||
}
|
||||
|
||||
const currentStatus = computed(() => String(props.listRow?.donationStatus || ''))
|
||||
const config = computed(() => ACTION_CONFIG[props.actionType])
|
||||
const actionLabel = computed(() => config.value.label)
|
||||
const timeLabel = computed(() => config.value.timeLabel)
|
||||
const descLabel = computed(() => config.value.descLabel)
|
||||
|
||||
const availableOperations = computed(() => {
|
||||
const ops: Array<{ value: OperationType; label: string }> = []
|
||||
const s = currentStatus.value
|
||||
|
||||
if (s === 'dsx') {
|
||||
ops.push({ value: 'activate', label: '生效' })
|
||||
ops.push({ value: 'void', label: '作废' })
|
||||
} else if (s === 'sxz') {
|
||||
ops.push({ value: 'expire', label: '过期' })
|
||||
ops.push({ value: 'void', label: '作废' })
|
||||
ops.push({ value: 'zckc', label: '转细胞存储' })
|
||||
}
|
||||
|
||||
return ops
|
||||
const dialogTitle = computed(() => {
|
||||
const label = config.value.label
|
||||
return props.targets.length > 1 ? `批量${label}(${props.targets.length} 条)` : label
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitting = ref(false)
|
||||
const form = reactive({
|
||||
operationType: '' as OperationType | '',
|
||||
changeTime: '',
|
||||
changeDesc: ''
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
operationType: [{ required: true, message: '请选择变更操作', trigger: 'change' }],
|
||||
changeTime: [{ required: true, message: '请选择时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const config = computed(() => {
|
||||
if (!form.operationType) return null
|
||||
return OPERATION_CONFIG[form.operationType]
|
||||
})
|
||||
|
||||
const timeLabel = computed(() => config.value?.timeLabel || '时间')
|
||||
|
||||
const dialogTitle = computed(() => '捐赠协议单 — 状态变更')
|
||||
|
||||
function handleOperationChange() {
|
||||
form.changeTime = ''
|
||||
form.changeDesc = ''
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.operationType = ''
|
||||
form.changeTime = ''
|
||||
form.changeDesc = ''
|
||||
formRef.value?.clearValidate()
|
||||
@@ -184,12 +162,60 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid || !props.donationId || !form.operationType) return
|
||||
if (!valid || !props.targets.length) return
|
||||
|
||||
const cfg = config.value
|
||||
const rows = [...props.targets]
|
||||
const isBatch = rows.length > 1
|
||||
|
||||
// 转细胞存储:确认 + 调用专用接口
|
||||
if (props.actionType === 'toStorage') {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定将该捐赠协议转为细胞存储吗?将创建新的细胞存储协议,初始协议号与样本编码与源单一致。',
|
||||
'转细胞存储确认',
|
||||
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
await withFullscreenLoading(
|
||||
async () => {
|
||||
let ok = 0
|
||||
const failed: string[] = []
|
||||
for (const row of rows) {
|
||||
try {
|
||||
await fetchConvertToStorage(String(row.id))
|
||||
ok += 1
|
||||
} catch {
|
||||
failed.push(row.donationNo || String(row.id))
|
||||
}
|
||||
}
|
||||
if (isBatch) {
|
||||
if (failed.length) {
|
||||
ElMessage.warning(`${cfg.label}成功 ${ok} 条,失败 ${failed.length} 条`)
|
||||
} else {
|
||||
ElMessage.success(`批量${cfg.label}成功,共 ${ok} 条`)
|
||||
}
|
||||
}
|
||||
if (ok > 0) {
|
||||
emit('update:modelValue', false)
|
||||
emit('submitted')
|
||||
}
|
||||
},
|
||||
isBatch ? fullscreenLoadingText.batchAudit : fullscreenLoadingText.save
|
||||
)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const cfg = OPERATION_CONFIG[form.operationType]
|
||||
const payload = {
|
||||
donationStatus: cfg.targetStatus,
|
||||
time: form.changeTime,
|
||||
newStatus: cfg.status,
|
||||
timeVal: form.changeTime,
|
||||
desc: form.changeDesc.trim() || undefined
|
||||
}
|
||||
|
||||
@@ -197,11 +223,29 @@
|
||||
try {
|
||||
await withFullscreenLoading(
|
||||
async () => {
|
||||
await fetchDonationStatusChange(props.donationId!, payload)
|
||||
emit('update:modelValue', false)
|
||||
emit('saved')
|
||||
let ok = 0
|
||||
const failed: string[] = []
|
||||
for (const row of rows) {
|
||||
try {
|
||||
await fetchDonationStatusChange(String(row.id), payload)
|
||||
ok += 1
|
||||
} catch {
|
||||
failed.push(row.donationNo || String(row.id))
|
||||
}
|
||||
}
|
||||
if (isBatch) {
|
||||
if (failed.length) {
|
||||
ElMessage.warning(`${cfg.label}成功 ${ok} 条,失败 ${failed.length} 条`)
|
||||
} else {
|
||||
ElMessage.success(`批量${cfg.label}成功,共 ${ok} 条`)
|
||||
}
|
||||
}
|
||||
if (ok > 0) {
|
||||
emit('update:modelValue', false)
|
||||
emit('submitted')
|
||||
}
|
||||
},
|
||||
fullscreenLoadingText.save
|
||||
isBatch ? fullscreenLoadingText.batchAudit : fullscreenLoadingText.save
|
||||
)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
@@ -221,8 +265,4 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.convert-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -18,7 +18,7 @@
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="捐赠编号/备注" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-form-item label="协议状态">
|
||||
<el-select
|
||||
v-model="searchForm.donationStatus"
|
||||
clearable
|
||||
@@ -89,6 +89,17 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="donationStatus"
|
||||
label="状态"
|
||||
width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{
|
||||
donationStatusLabel(String(row.donationStatus || ''))
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="donationDate"
|
||||
label="捐赠日期"
|
||||
@@ -104,29 +115,90 @@
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="donationStatus"
|
||||
label="状态"
|
||||
width="120"
|
||||
prop="motherPhone"
|
||||
label="母亲电话"
|
||||
min-width="130"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(String(row.donationStatus || ''))" disable-transitions>
|
||||
{{ donationStatusLabel(String(row.donationStatus || '')) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sourceType"
|
||||
label="来源类型"
|
||||
width="120"
|
||||
prop="motherIdCard"
|
||||
label="母亲身份证号"
|
||||
min-width="190"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="fatherName"
|
||||
label="父亲姓名"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="newbornSex"
|
||||
label="新生儿性别"
|
||||
width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{
|
||||
sourceTypeLabel(String(row.sourceType || ''))
|
||||
newbornSexLabel(String(row.newbornSex || ''))
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="deliveryMode"
|
||||
label="分娩方式"
|
||||
width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{
|
||||
deliveryModeLabel(String(row.deliveryMode || ''))
|
||||
}}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="newbornWeightGram"
|
||||
label="新生儿体重(克)"
|
||||
width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="regionName"
|
||||
label="采集地区"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="hospitalName"
|
||||
label="采集医院"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="collectorUserName"
|
||||
label="采集专员"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="salesUserName"
|
||||
label="销售人员"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="sampleCollectTime"
|
||||
label="样本采集时间"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
@@ -148,12 +220,6 @@
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)"
|
||||
><el-icon><Edit /></el-icon>编辑</el-button
|
||||
>
|
||||
<el-button class="btn-op-item" @click="handleAudit(row)"
|
||||
><el-icon><CircleCheck /></el-icon>审核</el-button
|
||||
>
|
||||
<el-button class="btn-op-item" @click="handleStatusChange(row)"
|
||||
><el-icon><Switch /></el-icon>状态变更</el-button
|
||||
>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)"
|
||||
><el-icon><Delete /></el-icon>删除</el-button
|
||||
>
|
||||
@@ -188,42 +254,15 @@
|
||||
:list-row="viewRow"
|
||||
show-audit-history
|
||||
/>
|
||||
|
||||
<!-- 审核弹窗 -->
|
||||
<DonationAuditDialog
|
||||
v-model="auditVisible"
|
||||
:donation-id="auditDonationId"
|
||||
:list-row="auditRow"
|
||||
@saved="onAuditSaved"
|
||||
/>
|
||||
|
||||
<!-- 状态变更弹窗 -->
|
||||
<DonationStatusChangeDialog
|
||||
v-model="statusChangeVisible"
|
||||
:donation-id="statusChangeDonationId"
|
||||
:list-row="statusChangeRow"
|
||||
@saved="onStatusChangeSaved"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
View,
|
||||
Edit,
|
||||
Delete,
|
||||
Search,
|
||||
RefreshLeft,
|
||||
CircleCheck,
|
||||
Switch
|
||||
} from '@element-plus/icons-vue'
|
||||
import { Plus, View, Edit, Delete, Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
import DonationFormDialog from './DonationFormDialog.vue'
|
||||
import DonationViewDialog from './DonationViewDialog.vue'
|
||||
import DonationAuditDialog from './DonationAuditDialog.vue'
|
||||
import DonationStatusChangeDialog from './DonationStatusChangeDialog.vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import {
|
||||
fetchResearchDonationList,
|
||||
@@ -245,20 +284,15 @@
|
||||
return m[s] || s || '-'
|
||||
}
|
||||
|
||||
function statusTagType(s: string) {
|
||||
const m: Record<string, string> = {
|
||||
dsx: 'info',
|
||||
sxz: 'success',
|
||||
ygq: 'warning',
|
||||
yzf: 'danger',
|
||||
zckc: 'primary'
|
||||
}
|
||||
return m[s] || 'info'
|
||||
function newbornSexLabel(s: string) {
|
||||
if (s === '1') return '男'
|
||||
if (s === '0') return '女'
|
||||
return s || '-'
|
||||
}
|
||||
|
||||
function sourceTypeLabel(s: string) {
|
||||
if (s === 'direct') return '独立新建'
|
||||
if (s === 'from_storage') return '从存储转来'
|
||||
function deliveryModeLabel(s: string) {
|
||||
if (s === 'sc') return '顺产'
|
||||
if (s === 'pgc') return '剖宫产'
|
||||
return s || '-'
|
||||
}
|
||||
|
||||
@@ -327,7 +361,7 @@
|
||||
}
|
||||
|
||||
function handleEdit(row: ResearchDonationRow) {
|
||||
formDialogEditId.value = row.id as string
|
||||
formDialogEditId.value = row.id
|
||||
formDialogRow.value = row
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
@@ -336,36 +370,6 @@
|
||||
loadData()
|
||||
}
|
||||
|
||||
/* ====== 审核弹窗 ====== */
|
||||
const auditVisible = ref(false)
|
||||
const auditDonationId = ref<string | null>(null)
|
||||
const auditRow = ref<ResearchDonationRow | null>(null)
|
||||
|
||||
function handleAudit(row: ResearchDonationRow) {
|
||||
auditDonationId.value = String(row.id)
|
||||
auditRow.value = row
|
||||
auditVisible.value = true
|
||||
}
|
||||
|
||||
function onAuditSaved() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
/* ====== 状态变更弹窗 ====== */
|
||||
const statusChangeVisible = ref(false)
|
||||
const statusChangeDonationId = ref<string | null>(null)
|
||||
const statusChangeRow = ref<ResearchDonationRow | null>(null)
|
||||
|
||||
function handleStatusChange(row: ResearchDonationRow) {
|
||||
statusChangeDonationId.value = String(row.id)
|
||||
statusChangeRow.value = row
|
||||
statusChangeVisible.value = true
|
||||
}
|
||||
|
||||
function onStatusChangeSaved() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
/* ====== 删除 ====== */
|
||||
function handleDelete(row: ResearchDonationRow) {
|
||||
ElMessageBox.confirm(`确认删除捐赠编号「${row.donationNo}」?`, '删除确认', {
|
||||
|
||||
Reference in new issue
Block a user