提交
This commit is contained in:
1 parent
135dd550b6
commit
2a4767a014
76 files changed
+25160
-2306
No files matched your search
@@ -80,7 +80,7 @@
|
||||
const panelTitleClassList = panelTitleIconByText.map(([, cls]) => cls)
|
||||
const panelTitleBarClass = 'title-bar-heading'
|
||||
const panelTitleBarTextRules: RegExp[] = [
|
||||
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表|流程配置|流程列表|待办列表|已办列表|申请列表|客户列表|细胞样本类型列表|采集地区列表|采集协议号列表|样本编码列表|医院列表|协议单列表|审核记录列表|缴费确认列表|退费确认列表|分期预警列表|请假列表|洁净室列表|库位树|预约列表|缴费信息列表)/
|
||||
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表|流程配置|流程列表|待办列表|已办列表|申请列表|客户列表|细胞样本类型列表|采集地区列表|采集协议号列表|样本编码列表|医院列表|协议单列表|审核记录列表|缴费确认列表|退费确认列表|分期预警列表|请假列表|洁净室列表|库位树|预约列表|缴费信息列表|样本接收列表|接收检查项列表|公司列表|项目列表|工单列表|任务列表)/
|
||||
]
|
||||
|
||||
const refreshPanelTitleIcons = () => {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export type QcItemParam = {
|
||||
id?: string
|
||||
paramName: string
|
||||
paramCode: string
|
||||
paramUnit?: string
|
||||
paramType: string
|
||||
requiredFlag: string
|
||||
orderNum?: number
|
||||
}
|
||||
|
||||
export function fetchQcItemParams(itemSource: string, itemId: string) {
|
||||
return request.get<QcItemParam[]>({
|
||||
url: '/web/admin/qc-item-params',
|
||||
params: { itemSource, itemId }
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSaveQcItemParams(itemSource: string, itemId: string, params: QcItemParam[]) {
|
||||
return request.put<QcItemParam[]>({
|
||||
url: '/web/admin/qc-item-params',
|
||||
params: { itemSource, itemId },
|
||||
data: { params }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
type PageList<T> = {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export type QcLabTestItem = {
|
||||
id: string
|
||||
itemCode: string
|
||||
itemName: string
|
||||
description?: string
|
||||
status: string
|
||||
orderNum: number
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export function fetchQcLabTestItemList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
status?: string
|
||||
}) {
|
||||
return request.get<PageList<QcLabTestItem>>({
|
||||
url: '/web/admin/qc-lab-test-items',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchQcLabTestItemsActive() {
|
||||
return request.get<QcLabTestItem[]>({
|
||||
url: '/web/admin/qc-lab-test-items/active'
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCreateQcLabTestItem(data: {
|
||||
itemCode: string
|
||||
itemName: string
|
||||
description?: string
|
||||
status?: string
|
||||
orderNum?: number
|
||||
remark?: string
|
||||
}) {
|
||||
return request.post<QcLabTestItem>({
|
||||
url: '/web/admin/qc-lab-test-items',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateQcLabTestItem(
|
||||
id: string,
|
||||
data: Partial<{
|
||||
itemCode: string
|
||||
itemName: string
|
||||
description: string
|
||||
status: string
|
||||
orderNum: number
|
||||
remark: string
|
||||
}>
|
||||
) {
|
||||
return request.put<QcLabTestItem>({
|
||||
url: `/web/admin/qc-lab-test-items/${id}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDeleteQcLabTestItem(id: string) {
|
||||
return request.del({ url: `/web/admin/qc-lab-test-items/${id}` })
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export type QcTaskResultItem = {
|
||||
paramId?: string
|
||||
paramName: string
|
||||
paramCode?: string
|
||||
paramUnit?: string
|
||||
paramType: string
|
||||
paramValue: string
|
||||
}
|
||||
|
||||
export function fetchQcTaskResults(taskId: string) {
|
||||
return request.get<QcTaskResultItem[]>({
|
||||
url: `/web/admin/qc-tasks/${taskId}/results`
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCompleteTaskWithResult(
|
||||
taskId: string,
|
||||
data: { results: QcTaskResultItem[]; thirdPartyAttachments?: string }
|
||||
) {
|
||||
return request.post({
|
||||
url: `/web/admin/qc-tasks/${taskId}/complete-with-result`,
|
||||
data
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
type PageList<T> = {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export type QcTask = {
|
||||
id: string
|
||||
workOrderId: string
|
||||
workOrderNo?: string
|
||||
taskNo: string
|
||||
taskType: string
|
||||
itemCode: string
|
||||
itemName: string
|
||||
companyId?: string
|
||||
companyName?: string
|
||||
feeAmount?: number
|
||||
taskStatus: string
|
||||
thirdPartyAttachments?: string
|
||||
completedTime?: string
|
||||
completedBy?: string
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export type QcTaskDetail = QcTask & {
|
||||
protocolNo?: string
|
||||
sampleCode?: string
|
||||
attachmentFiles?: Array<{ id?: string; fileUrl: string; fileName?: string }>
|
||||
results?: Array<{
|
||||
paramName: string
|
||||
paramUnit?: string
|
||||
paramType: string
|
||||
paramValue: string
|
||||
}>
|
||||
}
|
||||
|
||||
export function fetchQcTaskList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
taskType?: string
|
||||
taskStatus?: string
|
||||
workOrderId?: string
|
||||
}) {
|
||||
return request.get<PageList<QcTask>>({
|
||||
url: '/web/admin/qc-tasks',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchQcTaskDetail(id: string) {
|
||||
return request.get<QcTaskDetail>({
|
||||
url: `/web/admin/qc-tasks/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchStartQcTask(id: string) {
|
||||
return request.post<QcTask>({
|
||||
url: `/web/admin/qc-tasks/${id}/start`
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCompleteQcTask(id: string) {
|
||||
return request.post<QcTask>({
|
||||
url: `/web/admin/qc-tasks/${id}/complete`
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateQcTaskAttachments(id: string, thirdPartyAttachments: string) {
|
||||
return request.put<QcTask>({
|
||||
url: `/web/admin/qc-tasks/${id}/attachments`,
|
||||
data: { thirdPartyAttachments }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
type PageList<T> = {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export type QcThirdPartyCompany = {
|
||||
id: string
|
||||
companyCode: string
|
||||
companyName: string
|
||||
contactName?: string
|
||||
contactPhone?: string
|
||||
address?: string
|
||||
invoiceTitle?: string
|
||||
taxId?: string
|
||||
bankName?: string
|
||||
bankAccount?: string
|
||||
status: string
|
||||
orderNum: number
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export function fetchQcThirdPartyCompanyList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
status?: string
|
||||
}) {
|
||||
return request.get<PageList<QcThirdPartyCompany>>({
|
||||
url: '/web/admin/qc-third-party-companies',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchQcThirdPartyCompanyOptions() {
|
||||
return request.get<Array<{ id: string; companyCode: string; companyName: string }>>({
|
||||
url: '/web/admin/qc-third-party-companies/options'
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCreateQcThirdPartyCompany(data: {
|
||||
companyCode: string
|
||||
companyName: string
|
||||
contactName?: string
|
||||
contactPhone?: string
|
||||
address?: string
|
||||
invoiceTitle?: string
|
||||
taxId?: string
|
||||
bankName?: string
|
||||
bankAccount?: string
|
||||
status?: string
|
||||
orderNum?: number
|
||||
remark?: string
|
||||
}) {
|
||||
return request.post<QcThirdPartyCompany>({
|
||||
url: '/web/admin/qc-third-party-companies',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateQcThirdPartyCompany(
|
||||
id: string,
|
||||
data: Partial<{
|
||||
companyCode: string
|
||||
companyName: string
|
||||
contactName: string
|
||||
contactPhone: string
|
||||
address: string
|
||||
invoiceTitle: string
|
||||
taxId: string
|
||||
bankName: string
|
||||
bankAccount: string
|
||||
status: string
|
||||
orderNum: number
|
||||
remark: string
|
||||
}>
|
||||
) {
|
||||
return request.put<QcThirdPartyCompany>({
|
||||
url: `/web/admin/qc-third-party-companies/${id}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDeleteQcThirdPartyCompany(id: string) {
|
||||
return request.del({ url: `/web/admin/qc-third-party-companies/${id}` })
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
type PageList<T> = {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export type QcThirdPartyItem = {
|
||||
id: string
|
||||
companyId: string
|
||||
companyName?: string
|
||||
itemCode: string
|
||||
itemName: string
|
||||
feeAmount: number
|
||||
status: string
|
||||
orderNum: number
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export function fetchQcThirdPartyItemList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
status?: string
|
||||
companyId?: string
|
||||
}) {
|
||||
return request.get<PageList<QcThirdPartyItem>>({
|
||||
url: '/web/admin/qc-third-party-items',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchQcThirdPartyItemsActive(companyId?: string) {
|
||||
return request.get<QcThirdPartyItem[]>({
|
||||
url: '/web/admin/qc-third-party-items/active',
|
||||
params: companyId ? { companyId } : undefined
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCreateQcThirdPartyItem(data: {
|
||||
companyId: string
|
||||
itemCode: string
|
||||
itemName: string
|
||||
feeAmount: number
|
||||
status?: string
|
||||
orderNum?: number
|
||||
remark?: string
|
||||
}) {
|
||||
return request.post<QcThirdPartyItem>({
|
||||
url: '/web/admin/qc-third-party-items',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateQcThirdPartyItem(
|
||||
id: string,
|
||||
data: Partial<{
|
||||
companyId: string
|
||||
itemCode: string
|
||||
itemName: string
|
||||
feeAmount: number
|
||||
status: string
|
||||
orderNum: number
|
||||
remark: string
|
||||
}>
|
||||
) {
|
||||
return request.put<QcThirdPartyItem>({
|
||||
url: `/web/admin/qc-third-party-items/${id}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDeleteQcThirdPartyItem(id: string) {
|
||||
return request.del({ url: `/web/admin/qc-third-party-items/${id}` })
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
type PageList<T> = {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export type QcWorkOrder = {
|
||||
id: string
|
||||
workOrderNo: string
|
||||
receptionId: string
|
||||
protocolSampleId: string
|
||||
collectionProtocolId: string
|
||||
protocolNo: string
|
||||
sampleCode: string
|
||||
workOrderStatus: string
|
||||
hasLab: boolean
|
||||
hasThirdParty: boolean
|
||||
configuredTime?: string
|
||||
configuredBy?: string
|
||||
thirdPartyFeeTotal?: number
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export type QcWorkOrderDetail = QcWorkOrder & {
|
||||
reception?: {
|
||||
receptionTime?: string
|
||||
handlerName?: string
|
||||
acceptConclusion?: string
|
||||
}
|
||||
labItems?: Array<{ id: string; labTestItemId: string; itemCode: string; itemName: string }>
|
||||
thirdPartyItems?: Array<{
|
||||
id: string
|
||||
thirdPartyItemId: string
|
||||
companyId: string
|
||||
companyName: string
|
||||
itemCode: string
|
||||
itemName: string
|
||||
feeAmount: number
|
||||
}>
|
||||
tasks?: Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
export function fetchQcWorkOrderList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
workOrderStatus?: string
|
||||
}) {
|
||||
return request.get<PageList<QcWorkOrder>>({
|
||||
url: '/web/admin/qc-work-orders',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchQcWorkOrderDetail(id: string) {
|
||||
return request.get<QcWorkOrderDetail>({
|
||||
url: `/web/admin/qc-work-orders/${id}`
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchConfigureQcWorkOrder(
|
||||
id: string,
|
||||
data: {
|
||||
hasLab: boolean
|
||||
hasThirdParty: boolean
|
||||
labItemIds: string[]
|
||||
thirdPartyItemIds: string[]
|
||||
}
|
||||
) {
|
||||
return request.post<QcWorkOrder>({
|
||||
url: `/web/admin/qc-work-orders/${id}/configure`,
|
||||
data
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
type PageList<T> = {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export type SampleReceptionCheckItem = {
|
||||
id: string
|
||||
itemCode: string
|
||||
itemName: string
|
||||
status: string
|
||||
orderNum: number
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export function fetchSampleReceptionCheckItemList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
status?: string
|
||||
}) {
|
||||
return request.get<PageList<SampleReceptionCheckItem>>({
|
||||
url: '/web/admin/sample-reception-check-items',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSampleReceptionCheckItemsActive() {
|
||||
return request.get<SampleReceptionCheckItem[]>({
|
||||
url: '/web/admin/sample-reception-check-items/active'
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchCreateSampleReceptionCheckItem(data: {
|
||||
itemCode: string
|
||||
itemName: string
|
||||
status?: string
|
||||
orderNum?: number
|
||||
remark?: string
|
||||
}) {
|
||||
return request.post<SampleReceptionCheckItem>({
|
||||
url: '/web/admin/sample-reception-check-items',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateSampleReceptionCheckItem(
|
||||
id: string,
|
||||
data: {
|
||||
itemCode?: string
|
||||
itemName?: string
|
||||
status?: string
|
||||
orderNum?: number
|
||||
remark?: string
|
||||
}
|
||||
) {
|
||||
return request.put<SampleReceptionCheckItem>({
|
||||
url: `/web/admin/sample-reception-check-items/${id}`,
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchDeleteSampleReceptionCheckItem(id: string) {
|
||||
return request.del({
|
||||
url: `/web/admin/sample-reception-check-items/${id}`
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
type PageList<T> = {
|
||||
list: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export function fetchSampleReceptionPendingList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
}) {
|
||||
return request.get<PageList<Record<string, unknown>>>({
|
||||
url: '/web/admin/sample-receptions/pending',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSampleReceptionReceivedList(params?: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
keyword?: string
|
||||
}) {
|
||||
return request.get<PageList<Record<string, unknown>>>({
|
||||
url: '/web/admin/sample-receptions/received',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchSampleReceptionDetail(receptionId: string) {
|
||||
return request.get<Record<string, unknown>>({
|
||||
url: `/web/admin/sample-receptions/${receptionId}`
|
||||
})
|
||||
}
|
||||
|
||||
export type SampleReceptionCheckResult = {
|
||||
id?: string
|
||||
checkItemId?: string
|
||||
itemCode: string
|
||||
itemName: string
|
||||
checked: boolean
|
||||
orderNum?: number
|
||||
}
|
||||
|
||||
export function fetchSampleReceptionSubmit(data: {
|
||||
protocolSampleId: string
|
||||
receptionTime: string
|
||||
handlerUserId?: string
|
||||
handlerName?: string
|
||||
receiveTemperature?: string
|
||||
volumeMl?: number
|
||||
netWeightG?: number
|
||||
checks: Array<{ checkItemId: string; checked: boolean }>
|
||||
acceptConclusion: 'js' | 'bj'
|
||||
receiveAttachments?: string
|
||||
remark?: string
|
||||
}) {
|
||||
return request.post({
|
||||
url: '/web/admin/sample-receptions',
|
||||
data
|
||||
})
|
||||
}
|
||||
@@ -486,6 +486,7 @@ body {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.list-toolbar,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- 财务确认弹窗:附件上传 + 财务备注(对齐采集协议单附件 UI) -->
|
||||
<template>
|
||||
<div class="protocol-finance-confirm-fields">
|
||||
<el-form-item label="财务备注" class="form-col-span-2">
|
||||
<el-form-item :label="remarkLabel" class="form-col-span-2">
|
||||
<el-input
|
||||
:model-value="financeRemark"
|
||||
type="textarea"
|
||||
@@ -10,7 +10,7 @@
|
||||
@update:model-value="emit('update:financeRemark', $event)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="财务确认附件" class="form-col-span-2">
|
||||
<el-form-item :label="attachmentLabel" class="form-col-span-2">
|
||||
<div class="attachment-upload">
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
@@ -94,8 +94,14 @@
|
||||
financeRemark: string
|
||||
attachments: FinanceAttachmentItem[]
|
||||
uploadFolder?: string
|
||||
remarkLabel?: string
|
||||
attachmentLabel?: string
|
||||
}>(),
|
||||
{ uploadFolder: 'protocol-finance-confirm' }
|
||||
{
|
||||
uploadFolder: 'protocol-finance-confirm',
|
||||
remarkLabel: '财务备注',
|
||||
attachmentLabel: '财务确认附件'
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -144,6 +150,14 @@
|
||||
emit('update:attachments', next)
|
||||
}
|
||||
|
||||
/** Mutable snapshot used to avoid race conditions when multiple files are picked at once */
|
||||
let _pendingList: FinanceAttachmentItem[] | null = null
|
||||
|
||||
function _getList(): FinanceAttachmentItem[] {
|
||||
if (_pendingList) return _pendingList
|
||||
return props.attachments
|
||||
}
|
||||
|
||||
async function handleAttachmentChange(uploadFile: { raw?: File }) {
|
||||
const file = uploadFile.raw
|
||||
if (!file) return
|
||||
@@ -152,7 +166,7 @@
|
||||
ElMessage.error(`文件大小不能超过 ${maxSizeMb}MB`)
|
||||
return
|
||||
}
|
||||
const tempId = `temp_${Date.now()}`
|
||||
const tempId = `temp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
const uploading: FinanceAttachmentItem = {
|
||||
id: tempId,
|
||||
fileName: file.name,
|
||||
@@ -160,29 +174,38 @@
|
||||
fileType: file.type || '',
|
||||
_uploading: true
|
||||
}
|
||||
const listWithTemp = [...props.attachments, uploading]
|
||||
// Append to the latest snapshot so concurrent calls don't overwrite each other
|
||||
const current = _getList()
|
||||
const listWithTemp = [...current, uploading]
|
||||
_pendingList = listWithTemp
|
||||
emit('update:attachments', listWithTemp)
|
||||
const idx = listWithTemp.length - 1
|
||||
try {
|
||||
const fd = new FormData()
|
||||
fd.append('folder', props.uploadFolder)
|
||||
fd.append('file', file)
|
||||
const res = await fetchUploadAttachment(fd)
|
||||
const updated = [...listWithTemp]
|
||||
updated[idx] = {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
fileType: res.fileType || file.type || '',
|
||||
_uploading: false
|
||||
}
|
||||
// Read from latest snapshot (NOT the closure-captured listWithTemp) to avoid
|
||||
// overwriting sibling uploads that may have already completed while this one was in-flight
|
||||
const latest = _pendingList || props.attachments
|
||||
const updated = latest.map((a) =>
|
||||
a.id === tempId
|
||||
? {
|
||||
id: res.id || tempId,
|
||||
fileName: file.name || res.fileName,
|
||||
fileUrl: res.fileUrl,
|
||||
fileType: res.fileType || file.type || '',
|
||||
_uploading: false
|
||||
}
|
||||
: a
|
||||
)
|
||||
_pendingList = updated
|
||||
emit('update:attachments', updated)
|
||||
} catch {
|
||||
ElMessage.error('文件上传失败')
|
||||
emit(
|
||||
'update:attachments',
|
||||
props.attachments.filter((a) => a.id !== tempId)
|
||||
)
|
||||
const latest = _pendingList || props.attachments
|
||||
const remaining = latest.filter((a) => a.id !== tempId)
|
||||
_pendingList = remaining.length > 0 ? remaining : null
|
||||
emit('update:attachments', remaining)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -203,7 +226,7 @@
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 120px;
|
||||
width: 140px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 8px;
|
||||
@@ -236,18 +259,26 @@
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.4;
|
||||
line-height: 1;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 120px;
|
||||
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;
|
||||
@@ -263,58 +294,84 @@
|
||||
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;
|
||||
color: #64748b;
|
||||
|
||||
&--video {
|
||||
color: #10b981;
|
||||
background: #ede9fe;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
&--doc {
|
||||
background: #e0f2fe;
|
||||
color: #0284c7;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 6px 8px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.attachment-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: #334155;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
color: #334155;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
min-height: calc(11px * 1.4 * 2);
|
||||
}
|
||||
|
||||
.attachment-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
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;
|
||||
color: #94a3b8;
|
||||
font-size: 16px;
|
||||
background: #f1f5f9;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: #10b981;
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -46,7 +46,7 @@
|
||||
<dd>{{ formatPaymentDueDate(detail?.installmentDueDate) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>应缴费金额</dt>
|
||||
<dt>应缴费金额(元)</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.dueAmount) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
@@ -54,7 +54,7 @@
|
||||
<dd>{{ transferTypeLabel(String(detail?.transferType || '')) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
<dt>转账金额</dt>
|
||||
<dt>转账金额(元)</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.transferAmount) }}</dd>
|
||||
</div>
|
||||
<div class="view-kv">
|
||||
@@ -80,7 +80,7 @@
|
||||
<h4 v-if="refundApplyTitle" class="detail-block-title">{{ refundApplyTitle }}</h4>
|
||||
<dl class="view-kv-grid view-kv-grid--4">
|
||||
<div class="view-kv">
|
||||
<dt>退费金额</dt>
|
||||
<dt>退费金额(元)</dt>
|
||||
<dd>{{ formatPaymentAmount(detail?.refundAmount) }}</dd>
|
||||
</div>
|
||||
<div v-if="detail?.refundRemark" class="view-kv view-kv-span-2">
|
||||
|
||||
@@ -19,6 +19,97 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
menuCode: 'dashboard'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '100',
|
||||
name: 'System',
|
||||
path: '/system',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: '系统管理',
|
||||
icon: 'ri:settings-3-line',
|
||||
keepAlive: false,
|
||||
roles: ['admin']
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: '101',
|
||||
name: 'Tenant',
|
||||
path: 'tenant',
|
||||
component: '/system/tenant/index',
|
||||
meta: { title: '租户管理', keepAlive: true, menuCode: 'system:tenant:list' }
|
||||
},
|
||||
{
|
||||
id: '102',
|
||||
name: 'Dict',
|
||||
path: 'dict',
|
||||
component: '/system/dict/index',
|
||||
meta: { title: '字典管理', keepAlive: true, menuCode: 'system:dict:list' }
|
||||
},
|
||||
{
|
||||
id: '103',
|
||||
name: 'Dept',
|
||||
path: 'dept',
|
||||
component: '/system/dept/index',
|
||||
meta: { title: '组织管理', keepAlive: true, menuCode: 'system:dept:list' }
|
||||
},
|
||||
{
|
||||
id: '104',
|
||||
name: 'User',
|
||||
path: 'user',
|
||||
component: '/system/user/index',
|
||||
meta: { title: '用户管理', keepAlive: true, menuCode: 'system:user:list' }
|
||||
},
|
||||
{
|
||||
id: '10000000000000000000000000000008',
|
||||
name: 'UserDetail',
|
||||
path: 'user/detail',
|
||||
component: '/system/user/detail',
|
||||
meta: { title: '用户详情', isHide: true, keepAlive: true }
|
||||
},
|
||||
{
|
||||
id: '105',
|
||||
name: 'Role',
|
||||
path: 'role',
|
||||
component: '/system/role/index',
|
||||
meta: { title: '角色管理', keepAlive: true, menuCode: 'system:role:list' }
|
||||
},
|
||||
{
|
||||
id: '106',
|
||||
name: 'Menu',
|
||||
path: 'menu',
|
||||
component: '/system/menu/index',
|
||||
meta: { title: '菜单管理', keepAlive: true, menuCode: 'system:menu:list' }
|
||||
},
|
||||
{
|
||||
id: '107',
|
||||
name: 'Site',
|
||||
path: 'site',
|
||||
component: '/system/site/index',
|
||||
meta: { title: '站点管理', keepAlive: true, menuCode: 'system:site:list' }
|
||||
},
|
||||
{
|
||||
id: '108',
|
||||
name: 'Post',
|
||||
path: 'post',
|
||||
component: '/system/post/index',
|
||||
meta: { title: '职位管理', keepAlive: true, menuCode: 'system:post:list' }
|
||||
},
|
||||
{
|
||||
id: '109',
|
||||
name: 'Unit',
|
||||
path: 'unit',
|
||||
component: '/system/unit/index',
|
||||
meta: { title: '物品单位', keepAlive: true, menuCode: 'system:unit:list' }
|
||||
},
|
||||
{
|
||||
id: '110',
|
||||
name: 'Express',
|
||||
path: 'express',
|
||||
component: '/system/express/index',
|
||||
meta: { title: '快递公司', keepAlive: true, menuCode: 'system:express:list' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '60000000000000000000000000000001',
|
||||
name: 'Operations',
|
||||
@@ -159,6 +250,82 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '70000000000000000000000000000001',
|
||||
name: 'QualityInspection',
|
||||
path: '/quality-inspection',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: '质量检测',
|
||||
icon: 'ri:shield-check-line',
|
||||
keepAlive: false,
|
||||
roles: ['admin']
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: '70000000000000000000000000000002',
|
||||
name: 'QualityInspectionSampleReception',
|
||||
path: 'sample-reception',
|
||||
component: '/quality-inspection/sample-reception/index',
|
||||
meta: { title: '样本接收', keepAlive: true, menuCode: 'biz:sampleReception:list' }
|
||||
},
|
||||
{
|
||||
id: '70000000000000000000000000000003',
|
||||
name: 'QualityInspectionSampleReceptionCheckItem',
|
||||
path: 'sample-reception-check-item',
|
||||
component: '/quality-inspection/sample-reception-check-item/index',
|
||||
meta: {
|
||||
title: '接收检查项配置',
|
||||
keepAlive: true,
|
||||
menuCode: 'biz:sampleReceptionCheckItem:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '70000000000000000000000000000004',
|
||||
name: 'QualityInspectionThirdPartyCompany',
|
||||
path: 'third-party-company',
|
||||
component: '/quality-inspection/third-party-company/index',
|
||||
meta: {
|
||||
title: '第三方检测公司及项目',
|
||||
keepAlive: true,
|
||||
menuCode: 'biz:qcThirdPartyCompany:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '70000000000000000000000000000006',
|
||||
name: 'QualityInspectionLabTestItem',
|
||||
path: 'lab-test-item',
|
||||
component: '/quality-inspection/lab-test-item/index',
|
||||
meta: {
|
||||
title: '实验室自检项目',
|
||||
keepAlive: true,
|
||||
menuCode: 'biz:qcLabTestItem:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '70000000000000000000000000000007',
|
||||
name: 'QualityInspectionQcWorkOrder',
|
||||
path: 'qc-work-order',
|
||||
component: '/quality-inspection/qc-work-order/index',
|
||||
meta: {
|
||||
title: '质检工单',
|
||||
keepAlive: true,
|
||||
menuCode: 'biz:qcWorkOrder:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '70000000000000000000000000000008',
|
||||
name: 'QualityInspectionQcTask',
|
||||
path: 'qc-task',
|
||||
component: '/quality-inspection/qc-task/index',
|
||||
meta: {
|
||||
title: '质检任务',
|
||||
keepAlive: true,
|
||||
menuCode: 'biz:qcTask:list'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '30000000000000000000000000000001',
|
||||
name: 'Workflow',
|
||||
|
||||
Vendored
+4
@@ -542,6 +542,8 @@ declare namespace Api {
|
||||
id: string
|
||||
protocolNo: string
|
||||
issued: number
|
||||
/** 是否已在采集协议单主表占用(与号池「是否下发」无关) */
|
||||
usedInProtocol?: boolean
|
||||
issuedUserId: string
|
||||
issuedUserName: string
|
||||
issuedTime: string
|
||||
@@ -559,6 +561,8 @@ declare namespace Api {
|
||||
sampleCode: string
|
||||
sampleCodeType: string
|
||||
issued: number
|
||||
/** 是否已在协议单样本表中占用(与号池「是否下发」无关) */
|
||||
usedInProtocol?: boolean
|
||||
issuedUserId: string
|
||||
issuedUserName: string
|
||||
issuedTime: string
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/** 分期缴费:期数对齐、应缴日/金额计算(与 ProtocolFormDialog 逻辑一致) */
|
||||
|
||||
export function addMonthsYmd(dateStr: string, months: number): string {
|
||||
const compact = String(dateStr || '').replace(/-/g, '')
|
||||
if (compact.length !== 8) return ''
|
||||
const y = parseInt(compact.slice(0, 4), 10)
|
||||
const m = parseInt(compact.slice(4, 6), 10) - 1
|
||||
const d = parseInt(compact.slice(6, 8), 10)
|
||||
const date = new Date(y, m + months, d)
|
||||
const yy = date.getFullYear()
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(date.getDate()).padStart(2, '0')
|
||||
return `${yy}${mm}${dd}`
|
||||
}
|
||||
|
||||
export function calcInstallmentDueAmounts(
|
||||
contractAmount: number | null | undefined,
|
||||
count: number
|
||||
): number[] {
|
||||
if (contractAmount == null || !count || count < 1) return []
|
||||
const totalCents = Math.round(Number(contractAmount) * 100)
|
||||
const baseCents = Math.floor(totalCents / count)
|
||||
const amounts: number[] = []
|
||||
let sumCents = 0
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (i === count - 1) {
|
||||
amounts.push((totalCents - sumCents) / 100)
|
||||
} else {
|
||||
amounts.push(baseCents / 100)
|
||||
sumCents += baseCents
|
||||
}
|
||||
}
|
||||
return amounts
|
||||
}
|
||||
|
||||
type PaymentLike = Record<string, unknown> | null | undefined
|
||||
type DetailLike = Record<string, any>
|
||||
|
||||
/**
|
||||
* 分期模式下按 installmentCount 补齐缺失期次(保留已有明细数据),仅用于编辑表单。
|
||||
*/
|
||||
export function alignFqPaymentDetailsForDisplay(
|
||||
payment: PaymentLike,
|
||||
details: DetailLike[]
|
||||
): DetailLike[] {
|
||||
if (!payment || payment.payMode !== 'fq') return details
|
||||
const count = Number(payment.installmentCount)
|
||||
if (!count || count < 1) return details
|
||||
|
||||
const rows = [...details]
|
||||
const byNo = new Map<number, DetailLike>()
|
||||
const unnumbered: DetailLike[] = []
|
||||
for (const row of rows) {
|
||||
const no = row.installmentNo
|
||||
if (no != null && no !== '') {
|
||||
byNo.set(Number(no), row)
|
||||
} else {
|
||||
unnumbered.push(row)
|
||||
}
|
||||
}
|
||||
|
||||
const needed = new Set(Array.from({ length: count }, (_, i) => i + 1))
|
||||
const have = new Set(byNo.keys())
|
||||
let missing = false
|
||||
for (const n of needed) {
|
||||
if (!have.has(n)) {
|
||||
missing = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!missing && rows.length >= count) {
|
||||
return sortFqPaymentDetails(rows).slice(0, count)
|
||||
}
|
||||
|
||||
const contract = payment.contractAmount != null ? Number(payment.contractAmount) : null
|
||||
const amounts = calcInstallmentDueAmounts(contract, count)
|
||||
const startDate = String(payment.installmentStartDate || '')
|
||||
const interval = Number(payment.installmentIntervalMonths || 0)
|
||||
|
||||
const result: DetailLike[] = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const no = i + 1
|
||||
if (byNo.has(no)) {
|
||||
result.push(byNo.get(no)!)
|
||||
continue
|
||||
}
|
||||
if (i < unnumbered.length) {
|
||||
const raw = { ...unnumbered[i], installmentNo: no }
|
||||
if (!raw.installmentDueDate && startDate && interval) {
|
||||
raw.installmentDueDate = addMonthsYmd(startDate, i * interval)
|
||||
}
|
||||
result.push(raw)
|
||||
continue
|
||||
}
|
||||
result.push({
|
||||
installmentNo: no,
|
||||
installmentDueDate:
|
||||
startDate && interval ? addMonthsYmd(startDate, i * interval) : '',
|
||||
dueAmount: amounts[i] ?? null,
|
||||
payStatus: 'dfj',
|
||||
transferType: '',
|
||||
transferAmount: null,
|
||||
attachments: []
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function sortFqPaymentDetails(details: DetailLike[]): DetailLike[] {
|
||||
return [...details].sort((a, b) => {
|
||||
const na = a.installmentNo != null && a.installmentNo !== '' ? Number(a.installmentNo) : 9999
|
||||
const nb = b.installmentNo != null && b.installmentNo !== '' ? Number(b.installmentNo) : 9999
|
||||
if (na !== nb) return na - nb
|
||||
return Number(a.orderNum || 0) - Number(b.orderNum || 0)
|
||||
})
|
||||
}
|
||||
|
||||
/** 编辑表单:将缴费明细行数与分期期数对齐(不覆盖已有行) */
|
||||
export function syncFqPaymentDetailRowCount(
|
||||
payment: {
|
||||
payMode: string
|
||||
installmentCount: number | null
|
||||
contractAmount: number | null
|
||||
installmentStartDate: string
|
||||
installmentIntervalMonths: number | null
|
||||
},
|
||||
paymentDetails: DetailLike[]
|
||||
): void {
|
||||
if (payment.payMode !== 'fq') return
|
||||
const count = Number(payment.installmentCount)
|
||||
if (!count || count < 1) return
|
||||
|
||||
const aligned = alignFqPaymentDetailsForDisplay(payment, paymentDetails)
|
||||
paymentDetails.splice(0, paymentDetails.length, ...aligned)
|
||||
}
|
||||
@@ -78,5 +78,5 @@ export function formatPaymentAmount(v: unknown) {
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
return '¥' + n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
export function workOrderStatusLabel(v: string) {
|
||||
const m: Record<string, string> = {
|
||||
dpz: '待配置',
|
||||
ypz: '已配置',
|
||||
ywc: '已完成'
|
||||
}
|
||||
return m[v] || v || '—'
|
||||
}
|
||||
|
||||
export function taskTypeLabel(v: string) {
|
||||
const m: Record<string, string> = {
|
||||
sysz: '实验室自检',
|
||||
dsfjc: '第三方检测'
|
||||
}
|
||||
return m[v] || v || '—'
|
||||
}
|
||||
|
||||
export function taskStatusLabel(v: string) {
|
||||
const m: Record<string, string> = {
|
||||
dzx: '待执行',
|
||||
jxz: '进行中',
|
||||
ywc: '已完成'
|
||||
}
|
||||
return m[v] || v || '—'
|
||||
}
|
||||
@@ -34,11 +34,12 @@
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增类型
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAuth('delete')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@@ -116,8 +117,8 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
<el-button v-if="hasAuth('edit')" class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="hasAuth('delete')" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -218,6 +219,8 @@
|
||||
fetchDeleteCellSampleType
|
||||
} from '@/api/admin'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useAuth } from '@/hooks/core/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
defineOptions({ name: 'BizCellSampleType' })
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增协议号
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openBatchGenerate">
|
||||
<el-button v-if="hasAuth('add')" class="btn-ghost" @click="openBatchGenerate">
|
||||
批量生成
|
||||
</el-button>
|
||||
<el-button
|
||||
@@ -56,6 +56,7 @@
|
||||
批量收回
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAuth('delete')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@@ -137,8 +138,8 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
<el-button v-if="hasAuth('edit')" class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="hasAuth('delete')" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -376,6 +377,8 @@
|
||||
fetchBatchIssueProtocolNos,
|
||||
fetchBatchRevokeProtocolNos
|
||||
} from '@/api/admin'
|
||||
import { useAuth } from '@/hooks/core/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
defineOptions({ name: 'BizCollectionProtocolNo' })
|
||||
|
||||
|
||||
@@ -34,11 +34,12 @@
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增地区
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAuth('delete')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@@ -113,8 +114,8 @@
|
||||
<el-button class="btn-op-item primary" @click="openHospitalDialog(row)"
|
||||
>地区医院配置</el-button
|
||||
>
|
||||
<el-button class="btn-op-item" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
<el-button v-if="hasAuth('edit')" class="btn-op-item" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="hasAuth('delete')" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -345,6 +346,8 @@
|
||||
fetchUpdateHospital,
|
||||
fetchDeleteHospital
|
||||
} from '@/api/admin'
|
||||
import { useAuth } from '@/hooks/core/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
defineOptions({ name: 'BizCollectionRegion' })
|
||||
|
||||
|
||||
@@ -38,11 +38,12 @@
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增客户
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAuth('delete')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@@ -125,8 +126,8 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
<el-button v-if="hasAuth('edit')" class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="hasAuth('delete')" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -235,6 +236,8 @@
|
||||
fetchDeleteCustomer
|
||||
} from '@/api/admin'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useAuth } from '@/hooks/core/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
defineOptions({ name: 'BizCustomer' })
|
||||
|
||||
|
||||
@@ -34,11 +34,11 @@
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-button v-if="hasAuth('add')" class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增样本编码
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openBatchGenerate">
|
||||
<el-button v-if="hasAuth('add')" class="btn-ghost" @click="openBatchGenerate">
|
||||
批量生成
|
||||
</el-button>
|
||||
<el-button
|
||||
@@ -56,6 +56,7 @@
|
||||
批量收回
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasAuth('delete')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@@ -146,8 +147,8 @@
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
<el-button v-if="hasAuth('edit')" class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button v-if="hasAuth('delete')" class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -391,6 +392,8 @@
|
||||
fetchBatchIssueSampleCodes,
|
||||
fetchBatchRevokeSampleCodes
|
||||
} from '@/api/admin'
|
||||
import { useAuth } from '@/hooks/core/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
defineOptions({ name: 'BizSampleCode' })
|
||||
|
||||
|
||||
@@ -560,7 +560,7 @@
|
||||
if (v == null || v === '') return '-'
|
||||
const n = Number(v)
|
||||
if (Number.isNaN(n)) return String(v)
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
return '¥' + n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
|
||||
@@ -60,6 +60,18 @@
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
审核
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="openStatusChangeDialog('activate')">
|
||||
协议生效
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="openStatusChangeDialog('expire')">
|
||||
协议过期
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="openStatusChangeDialog('void')">
|
||||
协议作废
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="openStatusChangeDialog('toResearch')">
|
||||
协议转科研
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
||||
<el-icon><Delete /></el-icon>
|
||||
批量删除
|
||||
@@ -261,6 +273,14 @@
|
||||
@submitted="onAuditSubmitted"
|
||||
/>
|
||||
|
||||
<!-- 状态变更弹窗(生效/过期/作废/转科研) -->
|
||||
<ProtocolStatusChangeDialog
|
||||
v-model="statusChangeDialogVisible"
|
||||
:action-type="statusChangeAction"
|
||||
:targets="statusChangeTargets"
|
||||
@submitted="onStatusChangeSubmitted"
|
||||
/>
|
||||
|
||||
<!-- 查看弹窗 -->
|
||||
<ProtocolViewDialog
|
||||
v-model="viewVisible"
|
||||
@@ -288,6 +308,10 @@
|
||||
import ProtocolAuditDialog, {
|
||||
type ProtocolAuditTarget
|
||||
} from '../protocol/ProtocolAuditDialog.vue'
|
||||
import ProtocolStatusChangeDialog, {
|
||||
type StatusChangeAction,
|
||||
type StatusChangeTarget
|
||||
} from '../protocol/ProtocolStatusChangeDialog.vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import {
|
||||
fetchCollectionProtocolList,
|
||||
@@ -417,6 +441,36 @@
|
||||
loadData()
|
||||
}
|
||||
|
||||
/* ====== 状态变更(生效/过期/作废/转科研) ====== */
|
||||
const statusChangeDialogVisible = ref(false)
|
||||
const statusChangeAction = ref<StatusChangeAction>('activate')
|
||||
const statusChangeTargets = ref<StatusChangeTarget[]>([])
|
||||
|
||||
function openStatusChangeDialog(action: StatusChangeAction, row?: CollectionProtocolRow) {
|
||||
const rows = row ? [row] : selectedRows.value
|
||||
if (!rows.length) {
|
||||
const labels: Record<StatusChangeAction, string> = {
|
||||
activate: '协议生效',
|
||||
expire: '协议过期',
|
||||
void: '协议作废',
|
||||
toResearch: '协议转科研'
|
||||
}
|
||||
ElMessage.warning(`请先选择要操作的协议单`)
|
||||
return
|
||||
}
|
||||
statusChangeAction.value = action
|
||||
statusChangeTargets.value = rows.map((r) => ({
|
||||
id: String(r.id),
|
||||
protocolNo: r.protocolNo != null ? String(r.protocolNo) : undefined
|
||||
}))
|
||||
statusChangeDialogVisible.value = true
|
||||
}
|
||||
|
||||
function onStatusChangeSubmitted() {
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
}
|
||||
|
||||
/* ====== 删除 ====== */
|
||||
function handleDelete(row: CollectionProtocolRow) {
|
||||
ElMessageBox.confirm(`确认删除协议号「${row.protocolNo}」?`, '删除确认', {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,222 @@
|
||||
<!-- 采集协议单 — 状态变更弹窗(协议生效 / 协议过期 / 协议作废 / 协议转科研) -->
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="modelValue"
|
||||
:title="dialogTitle"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<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].protocolNo || '—' }}</strong>
|
||||
</p>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
class="biz-dialog-form"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item :label="timeLabel" prop="changeTime">
|
||||
<el-date-picker
|
||||
v-model="form.changeTime"
|
||||
type="datetime"
|
||||
placeholder="请选择时间"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="descLabel">
|
||||
<el-input
|
||||
v-model="form.changeDesc"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="可选"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitting" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<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 { fetchUpdateCollectionProtocol } from '@/api/collection-protocol'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
|
||||
export type StatusChangeAction = 'activate' | 'expire' | 'void' | 'toResearch'
|
||||
export type StatusChangeTarget = {
|
||||
id: string
|
||||
protocolNo?: string
|
||||
}
|
||||
|
||||
defineOptions({ name: 'ProtocolStatusChangeDialog' })
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
actionType: StatusChangeAction
|
||||
targets: StatusChangeTarget[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', val: boolean): void
|
||||
(e: 'submitted'): void
|
||||
}>()
|
||||
|
||||
const ACTION_CONFIG: Record<
|
||||
StatusChangeAction,
|
||||
{
|
||||
label: string
|
||||
timeLabel: string
|
||||
descLabel: string
|
||||
status: string
|
||||
timeKey: string
|
||||
descKey: string
|
||||
}
|
||||
> = {
|
||||
activate: {
|
||||
label: '协议生效',
|
||||
timeLabel: '协议生效时间',
|
||||
descLabel: '协议生效描述',
|
||||
status: 'sxz',
|
||||
timeKey: 'effectiveTime',
|
||||
descKey: 'effectiveDesc'
|
||||
},
|
||||
expire: {
|
||||
label: '协议过期',
|
||||
timeLabel: '协议过期时间',
|
||||
descLabel: '协议过期描述',
|
||||
status: 'ygq',
|
||||
timeKey: 'expireTime',
|
||||
descKey: 'expireDesc'
|
||||
},
|
||||
void: {
|
||||
label: '协议作废',
|
||||
timeLabel: '协议作废时间',
|
||||
descLabel: '协议作废描述',
|
||||
status: 'yzf',
|
||||
timeKey: 'voidTime',
|
||||
descKey: 'voidDesc'
|
||||
},
|
||||
toResearch: {
|
||||
label: '协议转科研',
|
||||
timeLabel: '协议转科研时间',
|
||||
descLabel: '协议转科研描述',
|
||||
status: 'zky',
|
||||
timeKey: 'toResearchTime',
|
||||
descKey: 'toResearchDesc'
|
||||
}
|
||||
}
|
||||
|
||||
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 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({
|
||||
changeTime: '',
|
||||
changeDesc: ''
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
changeTime: [{ required: true, message: '请选择时间', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.changeTime = ''
|
||||
form.changeDesc = ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid || !props.targets.length) return
|
||||
|
||||
const cfg = config.value
|
||||
const payload: Record<string, unknown> = {
|
||||
protocolStatus: cfg.status,
|
||||
[cfg.timeKey]: form.changeTime,
|
||||
[cfg.descKey]: form.changeDesc.trim() || undefined
|
||||
}
|
||||
const rows = [...props.targets]
|
||||
const isBatch = rows.length > 1
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await withFullscreenLoading(
|
||||
async () => {
|
||||
let ok = 0
|
||||
const failed: string[] = []
|
||||
for (const row of rows) {
|
||||
try {
|
||||
await fetchUpdateCollectionProtocol(String(row.id), payload)
|
||||
ok += 1
|
||||
} catch {
|
||||
failed.push(row.protocolNo || 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
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.status-change-hint {
|
||||
margin: 0 0 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
|
||||
strong {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -228,7 +228,9 @@
|
||||
<el-table-column prop="enterTime" label="进入时间" min-width="160" align="center" header-align="center" />
|
||||
<el-table-column prop="purpose" label="目的" min-width="120" align="center" header-align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="leaveTime" label="离开时间" min-width="160" align="center" header-align="center" />
|
||||
<el-table-column prop="bodyTemp" label="体温(℃)" width="90" align="center" header-align="center" />
|
||||
<el-table-column prop="bodyTemp" label="体温(℃)" width="90" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.bodyTemp != null ? row.bodyTemp + '℃' : '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleViewLog(row)"
|
||||
@@ -795,7 +797,7 @@
|
||||
{ label: '进入时间', value: row.enterTime },
|
||||
{ label: '离开时间', value: row.leaveTime },
|
||||
{ label: '目的', value: row.purpose, span: true },
|
||||
{ label: '体温(℃)', value: row.bodyTemp ? `${row.bodyTemp}` : '' },
|
||||
{ label: '体温(℃)', value: row.bodyTemp != null ? `${row.bodyTemp}℃` : '' },
|
||||
{ label: '备注', value: row.remark, span: true }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
<!-- 质量检测 — 实验室自检项目 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>实验室自检项目</h1>
|
||||
<p class="page-desc">维护本单位实验室可执行的检测项目,供质检工单配置时选择。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">项目列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="编码/名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100%">
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">已选 <span class="sel-num">{{ selectedRows.length }}</span> 项</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd"><el-icon><Plus /></el-icon>新增项目</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="(r: QcLabTestItem[]) => (selectedRows.value = r)"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="68" align="center" :index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1" />
|
||||
<el-table-column prop="itemCode" label="项目编码" min-width="140" align="center" />
|
||||
<el-table-column prop="itemName" label="项目名称" min-width="160" align="center" />
|
||||
<el-table-column prop="description" label="描述" min-width="200" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['tag', row.status === '1' ? 'success' : 'muted']">{{ row.status === '1' ? '启用' : '停用' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="320" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)"><el-icon><Edit /></el-icon>编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)"><el-icon><Delete /></el-icon>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(v) => ((pagination.page = 1), (pagination.pageSize = v), loadData())"
|
||||
@current-change="(v) => ((pagination.page = v), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="640px" class="dialog-form-layout" align-center destroy-on-close :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" class="biz-dialog-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="项目编码" prop="itemCode">
|
||||
<el-input v-model="formData.itemCode" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称" prop="itemName">
|
||||
<el-input v-model="formData.itemName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio value="1">启用</el-radio>
|
||||
<el-radio value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" class="form-col-span-2">
|
||||
<el-input v-model="formData.description" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<!-- 参数配置 -->
|
||||
<div class="form-section" style="margin-top: 8px">
|
||||
<div class="section-title">参数配置</div>
|
||||
<div class="param-row param-row-header">
|
||||
<span style="flex: 2">参数名称</span>
|
||||
<span style="flex: 1.5">参数编码</span>
|
||||
<span style="flex: 1">单位</span>
|
||||
<span style="flex: 1">类型</span>
|
||||
<span style="width: 40px; text-align: center">必填</span>
|
||||
<span style="width: 32px"></span>
|
||||
</div>
|
||||
<div v-if="itemParams.length === 0" style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px">
|
||||
暂无参数,点击下方按钮添加
|
||||
</div>
|
||||
<div v-for="(param, idx) in itemParams" :key="idx" class="param-row">
|
||||
<el-input v-model="param.paramName" placeholder="参数名称" style="flex: 2" />
|
||||
<el-input v-model="param.paramCode" placeholder="参数编码" style="flex: 1.5" />
|
||||
<el-input v-model="param.paramUnit" placeholder="单位" style="flex: 1" />
|
||||
<el-select v-model="param.paramType" style="flex: 1">
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="数字" value="number" />
|
||||
</el-select>
|
||||
<el-switch v-model="param.requiredFlag" active-value="1" inactive-value="0" />
|
||||
<el-button class="btn-op-item" @click="removeParam(idx)"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
<el-button type="primary" plain style="margin-top: 8px" @click="addParam"><el-icon><Plus /></el-icon>添加参数</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogVisible = false"><el-icon><Close /></el-icon>取消</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit"><el-icon><Check /></el-icon>确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
title="查看项目"
|
||||
:icon="Notebook"
|
||||
:sections="viewSections"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check, Search, RefreshLeft, View, Edit, Delete, Notebook } from '@element-plus/icons-vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import {
|
||||
fetchQcLabTestItemList,
|
||||
fetchCreateQcLabTestItem,
|
||||
fetchUpdateQcLabTestItem,
|
||||
fetchDeleteQcLabTestItem,
|
||||
type QcLabTestItem
|
||||
} from '@/api/qc-lab-test-item'
|
||||
import { fetchQcItemParams, fetchSaveQcItemParams, type QcItemParam } from '@/api/qc-item-param'
|
||||
|
||||
defineOptions({ name: 'QualityInspectionLabTestItem' })
|
||||
|
||||
const searchForm = reactive({ keyword: '', status: '' })
|
||||
const tableData = ref<QcLabTestItem[]>([])
|
||||
const selectedRows = ref<QcLabTestItem[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增项目')
|
||||
const isEdit = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const editingId = ref<string | null>(null)
|
||||
const itemParams = ref<QcItemParam[]>([])
|
||||
const formData = reactive({ itemCode: '', itemName: '', description: '', status: '1', remark: '' })
|
||||
const viewVisible = ref(false)
|
||||
const viewSections = ref<ViewSection[]>([])
|
||||
const formRules = {
|
||||
itemCode: [{ required: true, message: '请输入项目编码', trigger: 'blur' }],
|
||||
itemName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function addParam() {
|
||||
itemParams.value.push({
|
||||
paramName: '',
|
||||
paramCode: '',
|
||||
paramUnit: '',
|
||||
paramType: 'text',
|
||||
requiredFlag: '0'
|
||||
})
|
||||
}
|
||||
|
||||
function removeParam(index: number) {
|
||||
itemParams.value.splice(index, 1)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchQcLabTestItemList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
status: searchForm.status || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
function resetForm() {
|
||||
Object.assign(formData, { itemCode: '', itemName: '', description: '', status: '1', remark: '' })
|
||||
editingId.value = null
|
||||
itemParams.value = []
|
||||
}
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增项目'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
function handleEdit(row: QcLabTestItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑项目'
|
||||
editingId.value = row.id
|
||||
Object.assign(formData, {
|
||||
itemCode: row.itemCode,
|
||||
itemName: row.itemName,
|
||||
description: row.description || '',
|
||||
status: row.status || '1',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
fetchQcItemParams('lab', row.id).then((res) => {
|
||||
itemParams.value = res || []
|
||||
}).catch(() => {
|
||||
itemParams.value = []
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
async function handleSubmit() {
|
||||
if (!(await formRef.value?.validate?.().catch(() => false))) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = { ...formData, remark: formData.remark || undefined, description: formData.description || undefined }
|
||||
let savedId = editingId.value
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateQcLabTestItem(editingId.value, payload)
|
||||
ElMessage.success('已保存')
|
||||
} else {
|
||||
const created = await fetchCreateQcLabTestItem(payload)
|
||||
savedId = created?.id || ''
|
||||
ElMessage.success('已新增')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
if (savedId && itemParams.value.length > 0) {
|
||||
await fetchSaveQcItemParams('lab', savedId, itemParams.value).catch(() => {})
|
||||
}
|
||||
loadData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
function handleView(row: QcLabTestItem) {
|
||||
viewSections.value = [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '项目编码', value: row.itemCode },
|
||||
{ label: '项目名称', value: row.itemName },
|
||||
{ label: '状态', value: row.status === '1' ? '启用' : '停用' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '详细信息',
|
||||
fields: [
|
||||
{ label: '描述', value: row.description, span: true },
|
||||
{ label: '备注', value: row.remark, span: true }
|
||||
]
|
||||
}
|
||||
]
|
||||
viewVisible.value = true
|
||||
}
|
||||
async function handleDelete(row: QcLabTestItem) {
|
||||
await ElMessageBox.confirm(`确定删除「${row.itemName}」?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteQcLabTestItem(row.id)
|
||||
ElMessage.success('已删除')
|
||||
loadData()
|
||||
}
|
||||
onMounted(() => loadData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-section {
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.form-section .section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
border-left: 3px solid #10b981;
|
||||
}
|
||||
|
||||
.param-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.param-row-header {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,382 @@
|
||||
<!-- 质量检测 — 质检任务 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>质检任务</h1>
|
||||
<p class="page-desc">按检测项目跟进任务;实验室自检需开始/完成,第三方需上传检测材料后完成。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">任务列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="任务号/项目/工单号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="任务类型">
|
||||
<el-select v-model="searchForm.taskType" placeholder="全部" clearable style="width: 100%">
|
||||
<el-option label="实验室自检" value="sysz" />
|
||||
<el-option label="第三方检测" value="dsfjc" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务状态">
|
||||
<el-select v-model="searchForm.taskStatus" placeholder="全部" clearable style="width: 100%">
|
||||
<el-option label="待执行" value="dzx" />
|
||||
<el-option label="进行中" value="jxz" />
|
||||
<el-option label="已完成" value="ywc" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe @row-dblclick="handleView">
|
||||
<el-table-column type="index" label="序号" width="68" align="center" :index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1" />
|
||||
<el-table-column prop="taskNo" label="任务号" min-width="150" align="center" />
|
||||
<el-table-column prop="workOrderNo" label="工单号" min-width="140" align="center" />
|
||||
<el-table-column prop="taskType" label="类型" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['tag', row.taskType === 'sysz' ? 'success' : 'muted']">{{ taskTypeLabel(row.taskType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="itemName" label="检测项目" min-width="160" align="center" />
|
||||
<el-table-column prop="companyName" label="第三方公司" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.companyName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="taskStatus" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['tag', row.taskStatus === 'ywc' ? 'success' : row.taskStatus === 'jxz' ? 'warning' : 'muted']">
|
||||
{{ taskStatusLabel(row.taskStatus) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.taskType === 'sysz' && row.taskStatus === 'dzx'">
|
||||
<el-button class="btn-op-item primary" @click="handleStart(row)"><el-icon><VideoPlay /></el-icon>开始</el-button>
|
||||
</template>
|
||||
<template v-if="row.taskType === 'sysz' && row.taskStatus === 'jxz'">
|
||||
<el-button class="btn-op-item primary" @click="openResult(row)"><el-icon><EditPen /></el-icon>填写结果</el-button>
|
||||
</template>
|
||||
<template v-if="row.taskType === 'dsfjc' && row.taskStatus !== 'ywc'">
|
||||
<el-button class="btn-op-item" @click="openAttach(row)"><el-icon><Upload /></el-icon>上传材料</el-button>
|
||||
<el-button class="btn-op-item primary" @click="openResult(row)"><el-icon><EditPen /></el-icon>填写结果</el-button>
|
||||
</template>
|
||||
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(v) => ((pagination.page = 1), (pagination.pageSize = v), loadData())"
|
||||
@current-change="(v) => ((pagination.page = v), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="attachVisible" title="上传第三方检测材料" width="720px" class="dialog-form-layout" align-center destroy-on-close :close-on-click-modal="false">
|
||||
<ProtocolFinanceConfirmFields
|
||||
v-model:finance-remark="attachRemark"
|
||||
v-model:attachments="attachments"
|
||||
remark-label="备注"
|
||||
attachment-label="检测材料"
|
||||
upload-folder="qc-third-party"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="attachVisible = false"><el-icon><Close /></el-icon>取消</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="attachLoading" @click="saveAttachments"><el-icon><Check /></el-icon>保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="resultVisible"
|
||||
title="填写检测结果"
|
||||
width="720px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form label-position="top" class="biz-dialog-form">
|
||||
<div class="form-section">
|
||||
<div class="section-title">检测参数</div>
|
||||
<div v-if="resultParams.length === 0" style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px">
|
||||
该项目暂无配置参数
|
||||
</div>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item v-for="(param, idx) in resultParams" :key="idx" :label="param.paramName + (param.paramUnit ? `(${param.paramUnit})` : '')">
|
||||
<el-input v-if="param.paramType === 'text'" v-model="param.paramValue" placeholder="请输入" />
|
||||
<el-input-number v-else v-model="param.paramValue" :precision="2" style="width: 100%" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="resultTaskType === 'dsfjc'" style="margin-top: 12px">
|
||||
<ProtocolFinanceConfirmFields
|
||||
v-model:finance-remark="resultRemark"
|
||||
v-model:attachments="resultAttachments"
|
||||
remark-label="备注"
|
||||
attachment-label="检测材料"
|
||||
upload-folder="qc-third-party"
|
||||
/>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="resultVisible = false"><el-icon><Close /></el-icon>取消</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="resultLoading" @click="submitResult"><el-icon><Check /></el-icon>提交并完成</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
title="任务详情"
|
||||
:icon="Tickets"
|
||||
:meta="viewDetail?.taskNo || ''"
|
||||
:sections="viewSections"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
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 SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import {
|
||||
financeAttachmentsToUrlString,
|
||||
type FinanceAttachmentItem
|
||||
} from '@/components/business/protocol-finance-confirm-fields/finance-attachments'
|
||||
import {
|
||||
fetchQcTaskList,
|
||||
fetchQcTaskDetail,
|
||||
fetchStartQcTask,
|
||||
fetchCompleteQcTask,
|
||||
fetchUpdateQcTaskAttachments,
|
||||
type QcTask,
|
||||
type QcTaskDetail
|
||||
} from '@/api/qc-task'
|
||||
import { fetchQcTaskResults, fetchCompleteTaskWithResult, type QcTaskResultItem } from '@/api/qc-task-result'
|
||||
import { taskTypeLabel, taskStatusLabel } from '@/utils/qc-labels'
|
||||
|
||||
defineOptions({ name: 'QualityInspectionQcTask' })
|
||||
|
||||
const searchForm = reactive({ keyword: '', taskType: '', taskStatus: '' })
|
||||
const tableData = ref<QcTask[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
const attachVisible = ref(false)
|
||||
const attachLoading = ref(false)
|
||||
const attachTaskId = ref('')
|
||||
const attachRemark = ref('')
|
||||
const attachments = ref<FinanceAttachmentItem[]>([])
|
||||
|
||||
const resultVisible = ref(false)
|
||||
const resultLoading = ref(false)
|
||||
const resultTaskId = ref('')
|
||||
const resultTaskType = ref('')
|
||||
const resultParams = ref<QcTaskResultItem[]>([])
|
||||
const resultRemark = ref('')
|
||||
const resultAttachments = ref<FinanceAttachmentItem[]>([])
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewDetail = ref<QcTaskDetail | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
const resultSection =
|
||||
(d as any).results && (d as any).results.length > 0
|
||||
? [
|
||||
{
|
||||
title: '检测结果',
|
||||
fields: (d as any).results.map((r: any) => ({
|
||||
label: r.paramName + (r.paramUnit ? `(${r.paramUnit})` : ''),
|
||||
value: r.paramValue || '—'
|
||||
}))
|
||||
}
|
||||
]
|
||||
: []
|
||||
return [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '任务号', value: d.taskNo },
|
||||
{ label: '工单号', value: d.workOrderNo },
|
||||
{ label: '类型', value: taskTypeLabel(d.taskType) },
|
||||
{ label: '检测项目', value: d.itemName },
|
||||
{ label: '第三方公司', value: d.companyName || '—' },
|
||||
{ label: '状态', value: taskStatusLabel(d.taskStatus) }
|
||||
]
|
||||
},
|
||||
...resultSection
|
||||
]
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchQcTaskList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
taskType: searchForm.taskType || undefined,
|
||||
taskStatus: searchForm.taskStatus || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.taskType = ''
|
||||
searchForm.taskStatus = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleStart(row: QcTask) {
|
||||
await fetchStartQcTask(row.id)
|
||||
ElMessage.success('已开始')
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleComplete(row: QcTask) {
|
||||
await fetchCompleteQcTask(row.id)
|
||||
ElMessage.success('已完成')
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openAttach(row: QcTask) {
|
||||
attachTaskId.value = row.id
|
||||
attachRemark.value = ''
|
||||
attachments.value = []
|
||||
attachVisible.value = true
|
||||
}
|
||||
|
||||
async function saveAttachments() {
|
||||
if (attachments.value.some((a) => a._uploading)) {
|
||||
ElMessage.warning('附件正在上传,请稍候')
|
||||
return
|
||||
}
|
||||
attachLoading.value = true
|
||||
try {
|
||||
await fetchUpdateQcTaskAttachments(
|
||||
attachTaskId.value,
|
||||
financeAttachmentsToUrlString(attachments.value)
|
||||
)
|
||||
ElMessage.success('已保存')
|
||||
attachVisible.value = false
|
||||
loadData()
|
||||
} finally {
|
||||
attachLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleView(row: QcTask) {
|
||||
const detail = await fetchQcTaskDetail(row.id)
|
||||
try {
|
||||
const results = await fetchQcTaskResults(row.id)
|
||||
;(detail as any).results = results || []
|
||||
} catch {
|
||||
;(detail as any).results = []
|
||||
}
|
||||
viewDetail.value = detail
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
async function openResult(row: QcTask) {
|
||||
resultTaskId.value = row.id
|
||||
resultTaskType.value = row.taskType
|
||||
resultRemark.value = ''
|
||||
resultAttachments.value = []
|
||||
resultLoading.value = true
|
||||
try {
|
||||
const results = await fetchQcTaskResults(row.id)
|
||||
if (results && results.length > 0) {
|
||||
resultParams.value = results
|
||||
} else {
|
||||
resultParams.value = []
|
||||
}
|
||||
} catch {
|
||||
resultParams.value = []
|
||||
} finally {
|
||||
resultLoading.value = false
|
||||
}
|
||||
resultVisible.value = true
|
||||
}
|
||||
|
||||
async function submitResult() {
|
||||
if (resultAttachments.value.some((a) => a._uploading)) {
|
||||
ElMessage.warning('附件正在上传,请稍候')
|
||||
return
|
||||
}
|
||||
resultLoading.value = true
|
||||
try {
|
||||
const data: { results: QcTaskResultItem[]; thirdPartyAttachments?: string } = {
|
||||
results: resultParams.value.filter((p) => p.paramValue !== '' && p.paramValue !== undefined)
|
||||
}
|
||||
if (resultTaskType.value === 'dsfjc') {
|
||||
data.thirdPartyAttachments = financeAttachmentsToUrlString(resultAttachments.value)
|
||||
}
|
||||
await fetchCompleteTaskWithResult(resultTaskId.value, data)
|
||||
ElMessage.success('已完成')
|
||||
resultVisible.value = false
|
||||
loadData()
|
||||
} finally {
|
||||
resultLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.form-section {
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.form-section .section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
border-left: 3px solid #10b981;
|
||||
}
|
||||
|
||||
.biz-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,305 @@
|
||||
<!-- 质量检测 — 质检工单 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>质检工单</h1>
|
||||
<p class="page-desc">样本接收(结论为接收)后自动生成工单;在此配置检测方式与具体项目。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">工单列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="工单号/协议号/样本编码" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="工单状态">
|
||||
<el-select v-model="searchForm.workOrderStatus" placeholder="全部" clearable style="width: 100%">
|
||||
<el-option label="待配置" value="dpz" />
|
||||
<el-option label="已配置" value="ypz" />
|
||||
<el-option label="已完成" value="ywc" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset"><el-icon><RefreshLeft /></el-icon>重置</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch"><el-icon><Search /></el-icon>搜索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe @row-dblclick="handleView">
|
||||
<el-table-column type="index" label="序号" width="68" align="center" :index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1" />
|
||||
<el-table-column prop="workOrderNo" label="工单号" min-width="150" align="center" />
|
||||
<el-table-column prop="protocolNo" label="协议号" min-width="140" align="center" />
|
||||
<el-table-column prop="sampleCode" label="样本编码" min-width="120" align="center" />
|
||||
<el-table-column prop="workOrderStatus" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['tag', row.workOrderStatus === 'ywc' ? 'success' : row.workOrderStatus === 'ypz' ? 'warning' : 'muted']">
|
||||
{{ workOrderStatusLabel(row.workOrderStatus) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="检测方式" min-width="160" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.hasLab" class="tag success">实验室</span>
|
||||
<span v-if="row.hasThirdParty" class="tag muted" style="margin-left: 4px">第三方</span>
|
||||
<span v-if="!row.hasLab && !row.hasThirdParty">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||
<el-table-column label="操作" width="280" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
|
||||
<el-button v-if="row.workOrderStatus === 'dpz'" class="btn-op-item primary" @click="handleConfigure(row)"><el-icon><Setting /></el-icon>配置检测</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(v) => ((pagination.page = 1), (pagination.pageSize = v), loadData())"
|
||||
@current-change="(v) => ((pagination.page = v), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="configureVisible" title="配置检测" width="800px" class="dialog-form-layout" align-center destroy-on-close :close-on-click-modal="false">
|
||||
<el-form label-position="top" class="biz-dialog-form">
|
||||
<el-form-item label="检测方式">
|
||||
<el-checkbox v-model="configureForm.hasLab">实验室自检</el-checkbox>
|
||||
<el-checkbox v-model="configureForm.hasThirdParty" style="margin-left: 16px">第三方检测</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="configureForm.hasLab" label="实验室自检项目">
|
||||
<el-checkbox-group v-model="configureForm.labItemIds" class="qc-config-check-group">
|
||||
<el-checkbox v-for="item in labOptions" :key="item.id" :value="item.id">{{ item.itemName }}({{ item.itemCode }})</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<p v-if="!labOptions.length" class="hint-muted">暂无启用的实验室项目</p>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="configureForm.hasThirdParty" label="第三方检测项目">
|
||||
<el-checkbox-group v-model="configureForm.thirdPartyItemIds" class="qc-config-check-group">
|
||||
<div v-for="group in thirdOptionsGrouped" :key="group.companyId" class="qc-company-group">
|
||||
<div class="qc-company-group-title">{{ group.companyName }}</div>
|
||||
<div class="qc-company-group-items">
|
||||
<el-checkbox v-for="item in group.items" :key="item.id" :value="item.id">
|
||||
{{ item.itemName }}({{ item.itemCode }})¥{{ item.feeAmount }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
<p v-if="!thirdOptions.length" class="hint-muted">暂无启用的第三方项目</p>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="configureVisible = false"><el-icon><Close /></el-icon>取消</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="configureLoading" @click="submitConfigure"><el-icon><Check /></el-icon>确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
title="工单详情"
|
||||
:icon="Document"
|
||||
:meta="detail?.workOrderNo || ''"
|
||||
:sections="viewSections"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Close, Check, Search, RefreshLeft, View, Setting, Document } from '@element-plus/icons-vue'
|
||||
import { fetchQcLabTestItemsActive, type QcLabTestItem } from '@/api/qc-lab-test-item'
|
||||
import { fetchQcThirdPartyItemsActive, type QcThirdPartyItem } from '@/api/qc-third-party-item'
|
||||
import {
|
||||
fetchQcWorkOrderList,
|
||||
fetchQcWorkOrderDetail,
|
||||
fetchConfigureQcWorkOrder,
|
||||
type QcWorkOrder,
|
||||
type QcWorkOrderDetail
|
||||
} from '@/api/qc-work-order'
|
||||
import { workOrderStatusLabel } from '@/utils/qc-labels'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
|
||||
defineOptions({ name: 'QualityInspectionQcWorkOrder' })
|
||||
|
||||
const searchForm = reactive({ keyword: '', workOrderStatus: '' })
|
||||
const tableData = ref<QcWorkOrder[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
const configureVisible = ref(false)
|
||||
const configureLoading = ref(false)
|
||||
const configuringId = ref('')
|
||||
const labOptions = ref<QcLabTestItem[]>([])
|
||||
const thirdOptions = ref<QcThirdPartyItem[]>([])
|
||||
const thirdOptionsGrouped = computed(() => {
|
||||
const map = new Map<string, { companyId: string; companyName: string; items: QcThirdPartyItem[] }>()
|
||||
for (const item of thirdOptions.value) {
|
||||
const cid = item.companyId
|
||||
if (!map.has(cid)) {
|
||||
map.set(cid, { companyId: cid, companyName: item.companyName || '', items: [] })
|
||||
}
|
||||
map.get(cid)!.items.push(item)
|
||||
}
|
||||
return Array.from(map.values())
|
||||
})
|
||||
const configureForm = reactive({
|
||||
hasLab: false,
|
||||
hasThirdParty: false,
|
||||
labItemIds: [] as string[],
|
||||
thirdPartyItemIds: [] as string[]
|
||||
})
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const detail = ref<QcWorkOrderDetail | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
if (!detail.value) return []
|
||||
const d = detail.value
|
||||
return [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '工单号', value: d.workOrderNo },
|
||||
{ label: '协议号', value: d.protocolNo },
|
||||
{ label: '样本编码', value: d.sampleCode },
|
||||
{ label: '状态', value: workOrderStatusLabel(d.workOrderStatus) }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '实验室项目',
|
||||
fields: [
|
||||
{
|
||||
label: '检测项目',
|
||||
value: d.labItems?.map((li) => li.itemName + '(' + li.itemCode + ')').join('、') || '无',
|
||||
span: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '第三方项目',
|
||||
fields: [
|
||||
{
|
||||
label: '检测项目',
|
||||
value:
|
||||
d.thirdPartyItems?.map((li) => li.companyName + ' — ' + li.itemName + '(¥' + li.feeAmount + ')').join('、') ||
|
||||
'无',
|
||||
span: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchQcWorkOrderList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
workOrderStatus: searchForm.workOrderStatus || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.workOrderStatus = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleConfigure(row: QcWorkOrder) {
|
||||
configuringId.value = row.id
|
||||
configureForm.hasLab = false
|
||||
configureForm.hasThirdParty = false
|
||||
configureForm.labItemIds = []
|
||||
configureForm.thirdPartyItemIds = []
|
||||
labOptions.value = (await fetchQcLabTestItemsActive()) || []
|
||||
thirdOptions.value = (await fetchQcThirdPartyItemsActive()) || []
|
||||
configureVisible.value = true
|
||||
}
|
||||
|
||||
async function submitConfigure() {
|
||||
if (!configureForm.hasLab && !configureForm.hasThirdParty) {
|
||||
ElMessage.warning('请至少选择一种检测方式')
|
||||
return
|
||||
}
|
||||
configureLoading.value = true
|
||||
try {
|
||||
await fetchConfigureQcWorkOrder(configuringId.value, { ...configureForm })
|
||||
ElMessage.success('配置成功,已生成质检任务')
|
||||
configureVisible.value = false
|
||||
loadData()
|
||||
} finally {
|
||||
configureLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleView(row: QcWorkOrder) {
|
||||
detail.value = await fetchQcWorkOrderDetail(row.id)
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.qc-config-check-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px 16px;
|
||||
}
|
||||
|
||||
.qc-company-group {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.qc-company-group-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #334155;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px dashed #e2e8f0;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.qc-company-group-items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px 16px;
|
||||
}
|
||||
.hint-muted {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,309 @@
|
||||
<!-- 质量检测 — 样本接收检查项配置 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>接收检查项配置</h1>
|
||||
<p class="page-desc">维护样本接收登记时的检查项;仅「启用」项会出现在样本接收界面。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">接收检查项列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="编码/名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100%">
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">停用后新登记不再展示,历史接收记录仍按快照展示。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增检查项
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="itemCode" label="检查项编码" min-width="140" align="center" header-align="center" />
|
||||
<el-table-column prop="itemName" label="检查项名称" min-width="160" align="center" header-align="center" />
|
||||
<el-table-column prop="orderNum" label="排序" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="status" label="状态" width="90" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['tag', row.status === '1' ? 'success' : 'muted']">
|
||||
{{ row.status === '1' ? '启用' : '停用' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" header-align="center" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="640px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="biz-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="检查项编码" prop="itemCode">
|
||||
<el-input v-model="formData.itemCode" placeholder="租户内唯一,如 packaging" :disabled="isEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="检查项名称" prop="itemName">
|
||||
<el-input v-model="formData.itemName" placeholder="如:包装" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="formData.orderNum" :min="0" :max="9999" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio value="1">启用</el-radio>
|
||||
<el-radio value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check, Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchSampleReceptionCheckItemList,
|
||||
fetchCreateSampleReceptionCheckItem,
|
||||
fetchUpdateSampleReceptionCheckItem,
|
||||
fetchDeleteSampleReceptionCheckItem,
|
||||
type SampleReceptionCheckItem
|
||||
} from '@/api/sample-reception-check-item'
|
||||
|
||||
defineOptions({ name: 'QualityInspectionSampleReceptionCheckItem' })
|
||||
|
||||
const searchForm = reactive({ keyword: '', status: '' as string })
|
||||
const tableData = ref<SampleReceptionCheckItem[]>([])
|
||||
const selectedRows = ref<SampleReceptionCheckItem[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增检查项')
|
||||
const isEdit = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const editingId = ref<string | null>(null)
|
||||
|
||||
const formData = reactive({
|
||||
itemCode: '',
|
||||
itemName: '',
|
||||
orderNum: 0,
|
||||
status: '1',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
itemCode: [{ required: true, message: '请输入检查项编码', trigger: 'blur' }],
|
||||
itemName: [{ required: true, message: '请输入检查项名称', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: SampleReceptionCheckItem[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchSampleReceptionCheckItemList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
status: searchForm.status || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.itemCode = ''
|
||||
formData.itemName = ''
|
||||
formData.orderNum = 0
|
||||
formData.status = '1'
|
||||
formData.remark = ''
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增检查项'
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row: SampleReceptionCheckItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑检查项'
|
||||
editingId.value = row.id
|
||||
formData.itemCode = row.itemCode
|
||||
formData.itemName = row.itemName
|
||||
formData.orderNum = row.orderNum ?? 0
|
||||
formData.status = row.status || '1'
|
||||
formData.remark = row.remark || ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value?.validate?.().catch(() => false)
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
itemCode: formData.itemCode,
|
||||
itemName: formData.itemName,
|
||||
orderNum: formData.orderNum,
|
||||
status: formData.status,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateSampleReceptionCheckItem(editingId.value, payload)
|
||||
ElMessage.success('已保存')
|
||||
} else {
|
||||
await fetchCreateSampleReceptionCheckItem(payload)
|
||||
ElMessage.success('已新增')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: SampleReceptionCheckItem) {
|
||||
await ElMessageBox.confirm(`确定删除检查项「${row.itemName}」?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteSampleReceptionCheckItem(row.id)
|
||||
ElMessage.success('已删除')
|
||||
loadData()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
if (!selectedRows.value.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${selectedRows.value.length} 项?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
for (const row of selectedRows.value) {
|
||||
await fetchDeleteSampleReceptionCheckItem(row.id)
|
||||
}
|
||||
ElMessage.success('已删除')
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,723 @@
|
||||
<!-- 质量检测 — 第三方检测公司及项目 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>第三方检测公司及项目</h1>
|
||||
<p class="page-desc">维护外包检测公司信息,并在各公司下配置检测项目及费用。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">公司列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="关键词">
|
||||
<el-input v-model="searchForm.keyword" placeholder="编码/名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable style="width: 100%">
|
||||
<el-option label="启用" value="1" />
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增公司
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
:index="(i) => (pagination.page - 1) * pagination.pageSize + i + 1"
|
||||
/>
|
||||
<el-table-column prop="companyCode" label="公司编码" min-width="120" align="center" />
|
||||
<el-table-column prop="companyName" label="公司名称" min-width="160" align="center" />
|
||||
<el-table-column prop="contactName" label="联系人" min-width="100" align="center" />
|
||||
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['tag', row.status === '1' ? 'success' : 'muted']">
|
||||
{{ row.status === '1' ? '启用' : '停用' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="400" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="handleView(row)"><el-icon><View /></el-icon>查看</el-button>
|
||||
<el-button class="btn-op-item primary" @click="openItemDialog(row)"><el-icon><Setting /></el-icon>检测项目配置</el-button>
|
||||
<el-button class="btn-op-item" @click="handleEdit(row)"><el-icon><Edit /></el-icon>编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)"><el-icon><Delete /></el-icon>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(v) => ((pagination.page = 1), (pagination.pageSize = v), loadData())"
|
||||
@current-change="(v) => ((pagination.page = v), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="companyDialogVisible"
|
||||
:title="companyDialogTitle"
|
||||
width="720px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="companyFormRef" :model="companyForm" :rules="companyFormRules" label-position="top" class="biz-dialog-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="公司编码" prop="companyCode">
|
||||
<el-input v-model="companyForm.companyCode" :disabled="companyIsEdit" placeholder="租户内唯一" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公司名称" prop="companyName">
|
||||
<el-input v-model="companyForm.companyName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="companyForm.contactName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="companyForm.contactPhone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="地址" class="form-col-span-2">
|
||||
<el-input v-model="companyForm.address" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<div class="section-title">开票信息</div>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="发票抬头">
|
||||
<el-input v-model="companyForm.invoiceTitle" placeholder="与营业执照一致" />
|
||||
</el-form-item>
|
||||
<el-form-item label="纳税人识别号">
|
||||
<el-input v-model="companyForm.taxId" placeholder="统一社会信用代码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开户银行">
|
||||
<el-input v-model="companyForm.bankName" placeholder="收款银行全称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="银行账号">
|
||||
<el-input v-model="companyForm.bankAccount" placeholder="收款账号" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="companyForm.status">
|
||||
<el-radio value="1">启用</el-radio>
|
||||
<el-radio value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="companyForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="companyDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="companySubmitLoading" @click="submitCompany">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="itemDialogVisible"
|
||||
:title="itemDialogTitle"
|
||||
width="880px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@closed="onItemDialogClosed"
|
||||
>
|
||||
<div class="item-dialog-head">
|
||||
<span class="muted">当前公司:{{ currentCompanyName }}</span>
|
||||
</div>
|
||||
<div class="list-toolbar item-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ itemSelected.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAddItem">
|
||||
<el-icon><Plus /></el-icon>新增项目
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-body list-wrap item-table-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="itemTable"
|
||||
v-loading="itemLoading"
|
||||
stripe
|
||||
row-key="id"
|
||||
max-height="360"
|
||||
@selection-change="onItemSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" width="68" align="center" />
|
||||
<el-table-column prop="itemCode" label="项目编码" min-width="120" align="center" />
|
||||
<el-table-column prop="itemName" label="项目名称" min-width="160" align="center" />
|
||||
<el-table-column prop="feeAmount" label="费用(元)" width="110" align="center">
|
||||
<template #default="{ row }">{{ row.feeAmount != null ? `¥${row.feeAmount}` : '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :class="['tag', row.status === '1' ? 'success' : 'muted']">
|
||||
{{ row.status === '1' ? '启用' : '停用' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleEditItem(row)"><el-icon><Edit /></el-icon>编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDeleteItem(row)"><el-icon><Delete /></el-icon>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
:current-page="itemPagination.page"
|
||||
:page-size="itemPagination.pageSize"
|
||||
:total="itemPagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="(v) => ((itemPagination.page = 1), (itemPagination.pageSize = v), loadItems())"
|
||||
@current-change="(v) => ((itemPagination.page = v), loadItems())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="itemFormVisible"
|
||||
:title="itemFormTitle"
|
||||
width="640px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form ref="itemFormRef" :model="itemForm" :rules="itemFormRules" label-position="top" class="biz-dialog-form">
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="项目编码" prop="itemCode">
|
||||
<el-input v-model="itemForm.itemCode" :disabled="itemIsEdit" />
|
||||
</el-form-item>
|
||||
<el-form-item label="项目名称" prop="itemName">
|
||||
<el-input v-model="itemForm.itemName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="费用(元)" prop="feeAmount">
|
||||
<el-input-number v-model="itemForm.feeAmount" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="itemForm.status">
|
||||
<el-radio value="1">启用</el-radio>
|
||||
<el-radio value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="itemForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="form-section" style="margin-top: 8px">
|
||||
<div class="section-title">参数配置</div>
|
||||
<div class="param-row param-row-header">
|
||||
<span style="flex: 2">参数名称</span>
|
||||
<span style="flex: 1.5">参数编码</span>
|
||||
<span style="flex: 1">单位</span>
|
||||
<span style="flex: 1">类型</span>
|
||||
<span style="width: 40px; text-align: center">必填</span>
|
||||
<span style="width: 32px"></span>
|
||||
</div>
|
||||
<div v-if="itemParams.length === 0" style="text-align: center; padding: 8px 0; color: #94a3b8; font-size: 13px">
|
||||
暂无参数,点击下方按钮添加
|
||||
</div>
|
||||
<div v-for="(param, idx) in itemParams" :key="idx" class="param-row">
|
||||
<el-input v-model="param.paramName" placeholder="参数名称" style="flex: 2" />
|
||||
<el-input v-model="param.paramCode" placeholder="参数编码" style="flex: 1.5" />
|
||||
<el-input v-model="param.paramUnit" placeholder="单位" style="flex: 1" />
|
||||
<el-select v-model="param.paramType" style="flex: 1">
|
||||
<el-option label="文本" value="text" />
|
||||
<el-option label="数字" value="number" />
|
||||
</el-select>
|
||||
<el-switch v-model="param.requiredFlag" active-value="1" inactive-value="0" />
|
||||
<el-button class="btn-op-item" @click="removeItemParam(idx)"><el-icon><Delete /></el-icon></el-button>
|
||||
</div>
|
||||
<el-button type="primary" plain style="margin-top: 8px" @click="addItemParam"><el-icon><Plus /></el-icon>添加参数</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="itemFormVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="itemSubmitLoading" @click="submitItem">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="itemDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
title="查看公司"
|
||||
:icon="OfficeBuilding"
|
||||
:meta="viewCompanyName"
|
||||
:sections="viewSections"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check, Search, RefreshLeft, View, Setting, Edit, Delete, OfficeBuilding } from '@element-plus/icons-vue'
|
||||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||||
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import {
|
||||
fetchQcThirdPartyCompanyList,
|
||||
fetchCreateQcThirdPartyCompany,
|
||||
fetchUpdateQcThirdPartyCompany,
|
||||
fetchDeleteQcThirdPartyCompany,
|
||||
type QcThirdPartyCompany
|
||||
} from '@/api/qc-third-party-company'
|
||||
import {
|
||||
fetchQcThirdPartyItemList,
|
||||
fetchCreateQcThirdPartyItem,
|
||||
fetchUpdateQcThirdPartyItem,
|
||||
fetchDeleteQcThirdPartyItem,
|
||||
type QcThirdPartyItem
|
||||
} from '@/api/qc-third-party-item'
|
||||
import { fetchQcItemParams, fetchSaveQcItemParams, type QcItemParam } from '@/api/qc-item-param'
|
||||
|
||||
defineOptions({ name: 'QualityInspectionThirdPartyCompany' })
|
||||
|
||||
const searchForm = reactive({ keyword: '', status: '' })
|
||||
const tableData = ref<QcThirdPartyCompany[]>([])
|
||||
const selectedRows = ref<QcThirdPartyCompany[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
const companyDialogVisible = ref(false)
|
||||
const companyDialogTitle = ref('新增公司')
|
||||
const companyIsEdit = ref(false)
|
||||
const companySubmitLoading = ref(false)
|
||||
const companyFormRef = ref()
|
||||
const editingCompanyId = ref<string | null>(null)
|
||||
const companyForm = reactive({
|
||||
companyCode: '',
|
||||
companyName: '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
address: '',
|
||||
invoiceTitle: '',
|
||||
taxId: '',
|
||||
bankName: '',
|
||||
bankAccount: '',
|
||||
status: '1',
|
||||
remark: ''
|
||||
})
|
||||
const companyFormRules = {
|
||||
companyCode: [{ required: true, message: '请输入公司编码', trigger: 'blur' }],
|
||||
companyName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const itemDialogVisible = ref(false)
|
||||
const itemDialogTitle = ref('检测项目配置')
|
||||
const currentCompanyId = ref('')
|
||||
const currentCompanyName = ref('')
|
||||
const itemTable = ref<QcThirdPartyItem[]>([])
|
||||
const itemSelected = ref<QcThirdPartyItem[]>([])
|
||||
const itemLoading = ref(false)
|
||||
const itemPagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
|
||||
const itemFormVisible = ref(false)
|
||||
const itemFormTitle = ref('新增项目')
|
||||
const itemIsEdit = ref(false)
|
||||
const itemSubmitLoading = ref(false)
|
||||
const itemFormRef = ref()
|
||||
const editingItemId = ref<string | null>(null)
|
||||
const itemParams = ref<QcItemParam[]>([])
|
||||
const itemForm = reactive({
|
||||
itemCode: '',
|
||||
itemName: '',
|
||||
feeAmount: 0,
|
||||
status: '1',
|
||||
remark: ''
|
||||
})
|
||||
const itemFormRules = {
|
||||
itemCode: [{ required: true, message: '请输入项目编码', trigger: 'blur' }],
|
||||
itemName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
|
||||
feeAmount: [{ required: true, message: '请输入费用', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: QcThirdPartyCompany[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function onItemSelectionChange(rows: QcThirdPartyItem[]) {
|
||||
itemSelected.value = rows
|
||||
}
|
||||
|
||||
function addItemParam() {
|
||||
itemParams.value.push({
|
||||
paramName: '',
|
||||
paramCode: '',
|
||||
paramUnit: '',
|
||||
paramType: 'text',
|
||||
requiredFlag: '0'
|
||||
})
|
||||
}
|
||||
|
||||
function removeItemParam(index: number) {
|
||||
itemParams.value.splice(index, 1)
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchQcThirdPartyCompanyList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
status: searchForm.status || undefined
|
||||
})
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function resetCompanyForm() {
|
||||
companyForm.companyCode = ''
|
||||
companyForm.companyName = ''
|
||||
companyForm.contactName = ''
|
||||
companyForm.contactPhone = ''
|
||||
companyForm.address = ''
|
||||
companyForm.invoiceTitle = ''
|
||||
companyForm.taxId = ''
|
||||
companyForm.bankName = ''
|
||||
companyForm.bankAccount = ''
|
||||
companyForm.status = '1'
|
||||
companyForm.remark = ''
|
||||
editingCompanyId.value = null
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
companyIsEdit.value = false
|
||||
companyDialogTitle.value = '新增公司'
|
||||
resetCompanyForm()
|
||||
companyDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row: QcThirdPartyCompany) {
|
||||
companyIsEdit.value = true
|
||||
companyDialogTitle.value = '编辑公司'
|
||||
editingCompanyId.value = row.id
|
||||
companyForm.companyCode = row.companyCode
|
||||
companyForm.companyName = row.companyName
|
||||
companyForm.contactName = row.contactName || ''
|
||||
companyForm.contactPhone = row.contactPhone || ''
|
||||
companyForm.address = row.address || ''
|
||||
companyForm.invoiceTitle = row.invoiceTitle || ''
|
||||
companyForm.taxId = row.taxId || ''
|
||||
companyForm.bankName = row.bankName || ''
|
||||
companyForm.bankAccount = row.bankAccount || ''
|
||||
companyForm.status = row.status || '1'
|
||||
companyForm.remark = row.remark || ''
|
||||
companyDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitCompany() {
|
||||
const valid = await companyFormRef.value?.validate?.().catch(() => false)
|
||||
if (!valid) return
|
||||
companySubmitLoading.value = true
|
||||
try {
|
||||
const payload = { ...companyForm, remark: companyForm.remark || undefined }
|
||||
if (companyIsEdit.value && editingCompanyId.value) {
|
||||
await fetchUpdateQcThirdPartyCompany(editingCompanyId.value, payload)
|
||||
ElMessage.success('已保存')
|
||||
} else {
|
||||
await fetchCreateQcThirdPartyCompany(payload)
|
||||
ElMessage.success('已新增')
|
||||
}
|
||||
companyDialogVisible.value = false
|
||||
loadData()
|
||||
} finally {
|
||||
companySubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: QcThirdPartyCompany) {
|
||||
await ElMessageBox.confirm(`确定删除「${row.companyName}」?其下属检测项目将一并删除。`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await fetchDeleteQcThirdPartyCompany(row.id)
|
||||
ElMessage.success('已删除')
|
||||
loadData()
|
||||
}
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewCompanyName = ref('')
|
||||
const viewSections = ref<ViewSection[]>([])
|
||||
|
||||
function handleView(row: QcThirdPartyCompany) {
|
||||
viewCompanyName.value = row.companyName || ''
|
||||
viewSections.value = [
|
||||
{
|
||||
title: '基本信息',
|
||||
fields: [
|
||||
{ label: '公司编码', value: row.companyCode },
|
||||
{ label: '公司名称', value: row.companyName },
|
||||
{ label: '联系人', value: row.contactName },
|
||||
{ label: '联系电话', value: row.contactPhone },
|
||||
{ label: '地址', value: row.address, span: true },
|
||||
{ label: '状态', value: row.status === '1' ? '启用' : '停用' },
|
||||
{ label: '备注', value: row.remark, span: true }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '开票信息',
|
||||
fields: [
|
||||
{ label: '发票抬头', value: row.invoiceTitle },
|
||||
{ label: '纳税人识别号', value: row.taxId },
|
||||
{ label: '开户银行', value: row.bankName },
|
||||
{ label: '银行账号', value: row.bankAccount }
|
||||
]
|
||||
}
|
||||
]
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
function openItemDialog(row: QcThirdPartyCompany) {
|
||||
currentCompanyId.value = row.id
|
||||
currentCompanyName.value = row.companyName || ''
|
||||
itemPagination.page = 1
|
||||
itemSelected.value = []
|
||||
itemDialogTitle.value = `检测项目配置 — ${row.companyName}`
|
||||
itemDialogVisible.value = true
|
||||
loadItems()
|
||||
}
|
||||
|
||||
async function loadItems() {
|
||||
if (!currentCompanyId.value) return
|
||||
itemLoading.value = true
|
||||
try {
|
||||
const res = await fetchQcThirdPartyItemList({
|
||||
page: itemPagination.page,
|
||||
pageSize: itemPagination.pageSize,
|
||||
companyId: currentCompanyId.value
|
||||
})
|
||||
itemTable.value = res.list || []
|
||||
itemPagination.total = res.total ?? 0
|
||||
} finally {
|
||||
itemLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onItemDialogClosed() {
|
||||
currentCompanyId.value = ''
|
||||
currentCompanyName.value = ''
|
||||
itemTable.value = []
|
||||
}
|
||||
|
||||
function resetItemForm() {
|
||||
itemForm.itemCode = ''
|
||||
itemForm.itemName = ''
|
||||
itemForm.feeAmount = 0
|
||||
itemForm.status = '1'
|
||||
itemForm.remark = ''
|
||||
editingItemId.value = null
|
||||
itemParams.value = []
|
||||
}
|
||||
|
||||
function handleAddItem() {
|
||||
itemIsEdit.value = false
|
||||
itemFormTitle.value = '新增项目'
|
||||
resetItemForm()
|
||||
itemParams.value = []
|
||||
itemFormVisible.value = true
|
||||
}
|
||||
|
||||
function handleEditItem(row: QcThirdPartyItem) {
|
||||
itemIsEdit.value = true
|
||||
itemFormTitle.value = '编辑项目'
|
||||
editingItemId.value = row.id
|
||||
itemForm.itemCode = row.itemCode
|
||||
itemForm.itemName = row.itemName
|
||||
itemForm.feeAmount = row.feeAmount ?? 0
|
||||
itemForm.status = row.status || '1'
|
||||
itemForm.remark = row.remark || ''
|
||||
fetchQcItemParams('third_party', row.id).then((res) => {
|
||||
itemParams.value = res || []
|
||||
}).catch(() => {
|
||||
itemParams.value = []
|
||||
})
|
||||
itemFormVisible.value = true
|
||||
}
|
||||
|
||||
async function submitItem() {
|
||||
const valid = await itemFormRef.value?.validate?.().catch(() => false)
|
||||
if (!valid) return
|
||||
itemSubmitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
companyId: currentCompanyId.value,
|
||||
itemCode: itemForm.itemCode,
|
||||
itemName: itemForm.itemName,
|
||||
feeAmount: itemForm.feeAmount,
|
||||
status: itemForm.status,
|
||||
remark: itemForm.remark || undefined
|
||||
}
|
||||
let savedId = editingItemId.value
|
||||
if (itemIsEdit.value && editingItemId.value) {
|
||||
await fetchUpdateQcThirdPartyItem(editingItemId.value, payload)
|
||||
ElMessage.success('已保存')
|
||||
} else {
|
||||
const created = await fetchCreateQcThirdPartyItem(payload)
|
||||
savedId = created?.id || ''
|
||||
ElMessage.success('已新增')
|
||||
}
|
||||
if (savedId && itemParams.value.length > 0) {
|
||||
await fetchSaveQcItemParams('third_party', savedId, itemParams.value).catch(() => {})
|
||||
}
|
||||
itemFormVisible.value = false
|
||||
loadItems()
|
||||
} finally {
|
||||
itemSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteItem(row: QcThirdPartyItem) {
|
||||
await ElMessageBox.confirm(`确定删除「${row.itemName}」?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteQcThirdPartyItem(row.id)
|
||||
ElMessage.success('已删除')
|
||||
loadItems()
|
||||
}
|
||||
|
||||
onMounted(() => loadData())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.biz-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.item-dialog-head {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.item-dialog-head .muted {
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
.item-toolbar {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.item-table-wrap {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.form-section .section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
padding-left: 10px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
border-left: 3px solid #10b981;
|
||||
}
|
||||
|
||||
.param-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.param-row-header {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -179,6 +179,11 @@
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" style="grid-column: 1 / -1">
|
||||
<el-select v-model="formData.roleIds" multiple placeholder="请选择角色" style="width: 100%">
|
||||
<el-option v-for="item in roleOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" style="grid-column: 1 / -1">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
|
||||
</el-form-item>
|
||||
@@ -218,9 +223,20 @@
|
||||
fetchUpdateUser,
|
||||
fetchDeleteUser,
|
||||
fetchResetPassword,
|
||||
fetchDeptTree
|
||||
fetchDeptTree,
|
||||
fetchRoleOptions
|
||||
} from '@/api/admin'
|
||||
|
||||
const roleOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
|
||||
async function loadRoleOptions() {
|
||||
try {
|
||||
roleOptions.value = (await fetchRoleOptions()) || []
|
||||
} catch {
|
||||
roleOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
defineOptions({ name: 'SystemUser' })
|
||||
|
||||
const deptKeyword = ref('')
|
||||
@@ -251,7 +267,8 @@
|
||||
avatar: '',
|
||||
status: '1',
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
remark: '',
|
||||
roleIds: [] as string[]
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
@@ -327,6 +344,7 @@
|
||||
formData.status = '1'
|
||||
formData.orderNum = 0
|
||||
formData.remark = ''
|
||||
formData.roleIds = []
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
@@ -356,6 +374,7 @@
|
||||
formData.status = row.status || '1'
|
||||
formData.orderNum = row.orderNum ?? 0
|
||||
formData.remark = row.remark || ''
|
||||
formData.roleIds = (row as any).roleIds || []
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
@@ -373,7 +392,8 @@
|
||||
sex: formData.sex,
|
||||
status: formData.status,
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
remark: formData.remark?.trim() || undefined,
|
||||
roleIds: formData.roleIds.length ? formData.roleIds : undefined
|
||||
})
|
||||
} else {
|
||||
await fetchCreateUser({
|
||||
@@ -386,7 +406,8 @@
|
||||
sex: formData.sex,
|
||||
status: formData.status,
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
remark: formData.remark?.trim() || undefined,
|
||||
roleIds: formData.roleIds.length ? formData.roleIds : undefined
|
||||
})
|
||||
}
|
||||
dialogVisible.value = false
|
||||
@@ -445,6 +466,7 @@
|
||||
onMounted(() => {
|
||||
loadDeptTree()
|
||||
loadData()
|
||||
loadRoleOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in new issue
Block a user