提交
This commit is contained in:
1 parent
493f8d6756
commit
d84f4b944c
406 files changed
+58954
-10376
No files matched your search
@@ -9,3 +9,5 @@ export function list(data?: Record<string, any>) {
|
||||
export function getDetail(data: { id: string }) {
|
||||
return request.post({ url: '/web/monitor/operlog/detail', data })
|
||||
}
|
||||
|
||||
export const detail = getDetail
|
||||
@@ -1,8 +1,10 @@
|
||||
/** 操作日志视图对象 */
|
||||
export interface OperLogVO {
|
||||
id: number
|
||||
id: string | number
|
||||
title: string
|
||||
businessType: number
|
||||
businessId?: string
|
||||
operSummary?: string
|
||||
method?: string
|
||||
operName?: string
|
||||
operUrl?: string
|
||||
@@ -22,6 +24,27 @@ export interface OperLogQuery {
|
||||
pageSize?: number
|
||||
title?: string
|
||||
businessType?: number
|
||||
businessId?: string
|
||||
businessTitle?: string
|
||||
operName?: string
|
||||
status?: number
|
||||
}
|
||||
|
||||
export const OPER_LOG_BUSINESS_TYPE_OPTIONS = [
|
||||
{ label: '其他', value: 0, tagType: 'info' as const },
|
||||
{ label: '新增', value: 1, tagType: 'success' as const },
|
||||
{ label: '修改', value: 2, tagType: 'warning' as const },
|
||||
{ label: '删除', value: 3, tagType: 'danger' as const },
|
||||
{ label: '导出', value: 5, tagType: 'primary' as const },
|
||||
{ label: '导入', value: 6, tagType: 'primary' as const }
|
||||
]
|
||||
|
||||
export function getOperLogBusinessTypeLabel(value?: number) {
|
||||
const item = OPER_LOG_BUSINESS_TYPE_OPTIONS.find((o) => o.value === value)
|
||||
return item?.label ?? '其他'
|
||||
}
|
||||
|
||||
export function getOperLogBusinessTypeTagType(value?: number) {
|
||||
const item = OPER_LOG_BUSINESS_TYPE_OPTIONS.find((o) => o.value === value)
|
||||
return item?.tagType ?? 'info'
|
||||
}
|
||||
@@ -15,12 +15,23 @@ export interface Carrier {
|
||||
contactPhone?: string
|
||||
status: string
|
||||
remark?: string
|
||||
businessLicenseUrls?: string
|
||||
roadTransportLicenseUrls?: string
|
||||
transportAgreementUrls?: string
|
||||
transportAgreementExpireDate?: string
|
||||
auditStatus?: string
|
||||
auditBy?: string
|
||||
auditTime?: string
|
||||
auditRemark?: string
|
||||
qualificationComplete?: boolean
|
||||
transportAgreementExpired?: boolean
|
||||
createTime: string
|
||||
}
|
||||
|
||||
export interface CarrierQuery {
|
||||
q?: string
|
||||
status?: string
|
||||
auditStatus?: string
|
||||
}
|
||||
|
||||
export const carrierApi = {
|
||||
@@ -28,12 +39,16 @@ export const carrierApi = {
|
||||
get: (id: string) => request.get<Carrier>(`/transport/carrier/${id}`),
|
||||
create: (data: Partial<Carrier>) => request.post<Carrier>('/transport/carrier', data),
|
||||
update: (id: string, data: Partial<Carrier>) => request.put<Carrier>(`/transport/carrier/${id}`, data),
|
||||
approve: (id: string) => request.post<Carrier>(`/transport/carrier/${id}/approve`),
|
||||
reject: (id: string, data?: { remark?: string }) =>
|
||||
request.post<Carrier>(`/transport/carrier/${id}/reject`, data ?? {}),
|
||||
delete: (id: string) => request.delete(`/transport/carrier/${id}`)
|
||||
}
|
||||
|
||||
// 证书行(嵌套保存)
|
||||
export interface CertRow {
|
||||
certId?: string
|
||||
vehiclePart?: string
|
||||
certName: string
|
||||
certNo?: string
|
||||
expireDate?: string
|
||||
@@ -100,6 +115,7 @@ export interface VehicleQuery {
|
||||
vehicleType?: string
|
||||
carrierId?: string
|
||||
vehicleStatus?: string
|
||||
excludeIneligibleCarrier?: boolean
|
||||
}
|
||||
|
||||
export const vehicleApi = {
|
||||
@@ -620,6 +636,16 @@ export interface RemittanceVoucher {
|
||||
createTime?: string
|
||||
}
|
||||
|
||||
export interface FreightSettlementBrief {
|
||||
id: string
|
||||
settlementNo: string
|
||||
invoiceAmount?: number
|
||||
invoiceNo?: string
|
||||
invoiceDate?: string
|
||||
invoiceAttachmentUrls?: string
|
||||
settlementStatus: string
|
||||
}
|
||||
|
||||
export interface FreightSettlement {
|
||||
id: string
|
||||
settlementNo: string
|
||||
@@ -684,6 +710,8 @@ export interface FreightSettlementDisburseBody {
|
||||
export const freightApi = {
|
||||
listUnsettledPlans: (params?: UnsettledPlanQuery) =>
|
||||
request.get<UnsettledPlanListResponse>('/transport/freight/unsettled-plans', { params }),
|
||||
getByTransport: (transportId: string) =>
|
||||
request.get<FreightSettlementBrief | null>(`/transport/freight/by-transport/${transportId}`),
|
||||
list: (params?: FreightSettlementQuery) =>
|
||||
request.get<FreightSettlementListResponse>('/transport/freight/', { params }),
|
||||
get: (id: string) => request.get<FreightSettlement>(`/transport/freight/${id}`),
|
||||
@@ -799,3 +827,345 @@ export const inventoryApi = {
|
||||
updateDocumentReminder: (id: string, data: { status: string; remark?: string }) =>
|
||||
request.put<DocumentReminder>(`/transport/inventory/document-reminders/${id}`, data)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ETC/加油充值 API
|
||||
// =============================================================================
|
||||
|
||||
export interface VehicleRechargeApply {
|
||||
id: string
|
||||
applyNo: string
|
||||
licensePlate: string
|
||||
rechargeType: 'etc' | 'fuel'
|
||||
applyDate: string
|
||||
odometerKm?: number | null
|
||||
attachmentUrls?: string
|
||||
applicantId: string
|
||||
applicantName?: string
|
||||
status: 'pending' | 'disbursed' | 'cancelled'
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface VehicleRechargeApplyListResponse {
|
||||
items: VehicleRechargeApply[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface VehicleRechargeApplyOption {
|
||||
id: string
|
||||
applyNo: string
|
||||
licensePlate: string
|
||||
rechargeType: 'etc' | 'fuel'
|
||||
}
|
||||
|
||||
export interface VehicleRechargeRecord {
|
||||
id: string
|
||||
rechargeOrderNo: string
|
||||
rechargeDate: string
|
||||
rechargeType: 'etc' | 'fuel'
|
||||
cardNo?: string
|
||||
gasStation?: string
|
||||
amount: number
|
||||
attachmentUrls?: string
|
||||
applyId?: string
|
||||
applyNo?: string
|
||||
licensePlate?: string
|
||||
remark?: string
|
||||
createBy?: string
|
||||
createByName?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface VehicleRechargeRecordListResponse {
|
||||
items: VehicleRechargeRecord[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface VehicleRechargeDisburseBoardItem {
|
||||
applyId: string
|
||||
applyNo: string
|
||||
licensePlate: string
|
||||
rechargeType: 'etc' | 'fuel'
|
||||
applyDate: string
|
||||
odometerKm?: number | null
|
||||
applicantName?: string
|
||||
applyStatus: 'pending' | 'disbursed' | 'cancelled'
|
||||
applyRemark?: string
|
||||
recordId?: string
|
||||
rechargeOrderNo?: string
|
||||
rechargeDate?: string
|
||||
cardNo?: string
|
||||
gasStation?: string
|
||||
amount?: number | null
|
||||
recordCreateByName?: string
|
||||
recordRemark?: string
|
||||
}
|
||||
|
||||
export interface VehicleRechargeDisburseBoardListResponse {
|
||||
items: VehicleRechargeDisburseBoardItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export const vehicleRechargeApplyApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get<VehicleRechargeApplyListResponse>('/transport/vehicle-recharge-apply/', { params }),
|
||||
get: (id: string) => request.get<VehicleRechargeApply>(`/transport/vehicle-recharge-apply/${id}`),
|
||||
create: (data: Partial<VehicleRechargeApply>) =>
|
||||
request.post<VehicleRechargeApply>('/transport/vehicle-recharge-apply/', data),
|
||||
update: (id: string, data: Partial<VehicleRechargeApply>) =>
|
||||
request.put<VehicleRechargeApply>(`/transport/vehicle-recharge-apply/${id}`, data),
|
||||
cancel: (id: string) =>
|
||||
request.post<VehicleRechargeApply>(`/transport/vehicle-recharge-apply/${id}/cancel`),
|
||||
delete: (ids: string) => request.delete(`/transport/vehicle-recharge-apply/${ids}`),
|
||||
pendingOptions: () =>
|
||||
request.get<VehicleRechargeApplyOption[]>('/transport/vehicle-recharge-apply/pending-options')
|
||||
}
|
||||
|
||||
export const vehicleRechargeRecordApi = {
|
||||
disburseBoard: (params?: Record<string, unknown>) =>
|
||||
request.get<VehicleRechargeDisburseBoardListResponse>(
|
||||
'/transport/vehicle-recharge-record/disburse-board',
|
||||
{ params }
|
||||
),
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get<VehicleRechargeRecordListResponse>('/transport/vehicle-recharge-record/', { params }),
|
||||
get: (id: string) => request.get<VehicleRechargeRecord>(`/transport/vehicle-recharge-record/${id}`),
|
||||
create: (data: Partial<VehicleRechargeRecord>) =>
|
||||
request.post<VehicleRechargeRecord>('/transport/vehicle-recharge-record/', data),
|
||||
update: (id: string, data: Partial<VehicleRechargeRecord>) =>
|
||||
request.put<VehicleRechargeRecord>(`/transport/vehicle-recharge-record/${id}`, data),
|
||||
delete: (ids: string) => request.delete(`/transport/vehicle-recharge-record/${ids}`)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 车辆维保 API
|
||||
// =============================================================================
|
||||
|
||||
export interface VehicleMaintenanceApply {
|
||||
id: string
|
||||
applyNo: string
|
||||
licensePlate: string
|
||||
applicantId: string
|
||||
applicantName?: string
|
||||
applyDate: string
|
||||
maintenanceType: string
|
||||
startDate: string
|
||||
expectedPickupDate?: string | null
|
||||
maintenanceContent?: string
|
||||
shopAddress?: string
|
||||
attachmentUrls?: string
|
||||
status: 'pending' | 'processed' | 'cancelled'
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface VehicleMaintenanceApplyListResponse {
|
||||
items: VehicleMaintenanceApply[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface VehicleMaintenanceApplyReminder {
|
||||
id: string
|
||||
applyNo: string
|
||||
licensePlate: string
|
||||
maintenanceType: string
|
||||
applyDate: string
|
||||
startDate: string
|
||||
expectedPickupDate?: string | null
|
||||
maintenanceContent?: string
|
||||
shopAddress?: string
|
||||
}
|
||||
|
||||
export interface VehicleMaintenanceProcess {
|
||||
id: string
|
||||
processNo: string
|
||||
applyId?: string
|
||||
applyNo?: string
|
||||
licensePlate?: string
|
||||
amount?: number | null
|
||||
handlerId: string
|
||||
handlerName?: string
|
||||
processDate: string
|
||||
attachmentUrls?: string
|
||||
remark?: string
|
||||
createBy?: string
|
||||
createByName?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface VehicleMaintenanceProcessBoardItem {
|
||||
applyId: string
|
||||
applyNo: string
|
||||
licensePlate: string
|
||||
maintenanceType: string
|
||||
applyDate: string
|
||||
startDate: string
|
||||
expectedPickupDate?: string | null
|
||||
maintenanceContent?: string
|
||||
shopAddress?: string
|
||||
applicantName?: string
|
||||
applyStatus: 'pending' | 'processed' | 'cancelled'
|
||||
applyRemark?: string
|
||||
processId?: string
|
||||
processNo?: string
|
||||
processDate?: string
|
||||
amount?: number | null
|
||||
handlerName?: string
|
||||
processRemark?: string
|
||||
}
|
||||
|
||||
export interface VehicleMaintenanceProcessBoardListResponse {
|
||||
items: VehicleMaintenanceProcessBoardItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export const vehicleMaintenanceApplyApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get<VehicleMaintenanceApplyListResponse>('/transport/vehicle-maintenance-apply/', {
|
||||
params
|
||||
}),
|
||||
get: (id: string) =>
|
||||
request.get<VehicleMaintenanceApply>(`/transport/vehicle-maintenance-apply/${id}`),
|
||||
create: (data: Partial<VehicleMaintenanceApply>) =>
|
||||
request.post<VehicleMaintenanceApply>('/transport/vehicle-maintenance-apply/', data),
|
||||
update: (id: string, data: Partial<VehicleMaintenanceApply>) =>
|
||||
request.put<VehicleMaintenanceApply>(`/transport/vehicle-maintenance-apply/${id}`, data),
|
||||
cancel: (id: string) =>
|
||||
request.post<VehicleMaintenanceApply>(`/transport/vehicle-maintenance-apply/${id}/cancel`),
|
||||
delete: (ids: string) => request.delete(`/transport/vehicle-maintenance-apply/${ids}`),
|
||||
reminders: (licensePlate: string, excludeApplyId?: string) =>
|
||||
request.get<VehicleMaintenanceApplyReminder[]>('/transport/vehicle-maintenance-apply/reminders', {
|
||||
params: { licensePlate, excludeApplyId }
|
||||
})
|
||||
}
|
||||
|
||||
export const vehicleMaintenanceProcessApi = {
|
||||
processBoard: (params?: Record<string, unknown>) =>
|
||||
request.get<VehicleMaintenanceProcessBoardListResponse>(
|
||||
'/transport/vehicle-maintenance-process/process-board',
|
||||
{ params }
|
||||
),
|
||||
get: (id: string) =>
|
||||
request.get<VehicleMaintenanceProcess>(`/transport/vehicle-maintenance-process/${id}`),
|
||||
create: (data: Partial<VehicleMaintenanceProcess>) =>
|
||||
request.post<VehicleMaintenanceProcess>('/transport/vehicle-maintenance-process/', data),
|
||||
update: (id: string, data: Partial<VehicleMaintenanceProcess>) =>
|
||||
request.put<VehicleMaintenanceProcess>(`/transport/vehicle-maintenance-process/${id}`, data),
|
||||
delete: (ids: string) => request.delete(`/transport/vehicle-maintenance-process/${ids}`)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 车辆尿素 API
|
||||
// =============================================================================
|
||||
|
||||
export interface VehicleUreaApply {
|
||||
id: string
|
||||
applyNo: string
|
||||
licensePlate: string
|
||||
applicantId: string
|
||||
applicantName?: string
|
||||
applyDate: string
|
||||
warehouseInDate: string
|
||||
unit: string
|
||||
quantity: number
|
||||
applyAmount: number
|
||||
attachmentUrls?: string
|
||||
status: 'pending' | 'disbursed' | 'cancelled'
|
||||
remark?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface VehicleUreaApplyListResponse {
|
||||
items: VehicleUreaApply[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export interface VehicleUreaDisburse {
|
||||
id: string
|
||||
disburseNo: string
|
||||
applyId?: string
|
||||
applyNo?: string
|
||||
licensePlate?: string
|
||||
amount: number
|
||||
handlerId: string
|
||||
handlerName?: string
|
||||
disburseDate: string
|
||||
attachmentUrls?: string
|
||||
remark?: string
|
||||
createBy?: string
|
||||
createByName?: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface VehicleUreaDisburseBoardItem {
|
||||
applyId: string
|
||||
applyNo: string
|
||||
licensePlate: string
|
||||
applyDate: string
|
||||
warehouseInDate: string
|
||||
unit: string
|
||||
quantity: number
|
||||
applyAmount: number
|
||||
applicantName?: string
|
||||
applyStatus: 'pending' | 'disbursed' | 'cancelled'
|
||||
applyRemark?: string
|
||||
disburseId?: string
|
||||
disburseNo?: string
|
||||
disburseDate?: string
|
||||
amount?: number | null
|
||||
handlerName?: string
|
||||
disburseRemark?: string
|
||||
}
|
||||
|
||||
export interface VehicleUreaDisburseBoardListResponse {
|
||||
items: VehicleUreaDisburseBoardItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
export const vehicleUreaApplyApi = {
|
||||
list: (params?: Record<string, unknown>) =>
|
||||
request.get<VehicleUreaApplyListResponse>('/transport/vehicle-urea-apply/', { params }),
|
||||
get: (id: string) => request.get<VehicleUreaApply>(`/transport/vehicle-urea-apply/${id}`),
|
||||
create: (data: Partial<VehicleUreaApply>) =>
|
||||
request.post<VehicleUreaApply>('/transport/vehicle-urea-apply/', data),
|
||||
update: (id: string, data: Partial<VehicleUreaApply>) =>
|
||||
request.put<VehicleUreaApply>(`/transport/vehicle-urea-apply/${id}`, data),
|
||||
cancel: (id: string) =>
|
||||
request.post<VehicleUreaApply>(`/transport/vehicle-urea-apply/${id}/cancel`),
|
||||
delete: (ids: string) => request.delete(`/transport/vehicle-urea-apply/${ids}`)
|
||||
}
|
||||
|
||||
export const vehicleUreaDisburseApi = {
|
||||
disburseBoard: (params?: Record<string, unknown>) =>
|
||||
request.get<VehicleUreaDisburseBoardListResponse>(
|
||||
'/transport/vehicle-urea-disburse/disburse-board',
|
||||
{ params }
|
||||
),
|
||||
get: (id: string) => request.get<VehicleUreaDisburse>(`/transport/vehicle-urea-disburse/${id}`),
|
||||
create: (data: Partial<VehicleUreaDisburse>) =>
|
||||
request.post<VehicleUreaDisburse>('/transport/vehicle-urea-disburse/', data),
|
||||
update: (id: string, data: Partial<VehicleUreaDisburse>) =>
|
||||
request.put<VehicleUreaDisburse>(`/transport/vehicle-urea-disburse/${id}`, data),
|
||||
delete: (ids: string) => request.delete(`/transport/vehicle-urea-disburse/${ids}`)
|
||||
}
|
||||
@@ -116,7 +116,14 @@
|
||||
<DictTag dict-type="crm_contract_status" :value="row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="合同金额" min-width="120" align="right" header-align="center">
|
||||
<el-table-column
|
||||
v-if="!hideAmount"
|
||||
prop="amount"
|
||||
label="合同金额"
|
||||
min-width="120"
|
||||
align="right"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="money-display">{{ formatMoneyDisplay(row.amount) }}</span>
|
||||
</template>
|
||||
@@ -168,9 +175,15 @@ import DictTag from '@/components/DictTag/index.vue'
|
||||
import ListPagination from '@/components/ListPagination/index.vue'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
hideAmount?: boolean
|
||||
}>(),
|
||||
{
|
||||
hideAmount: false
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
|
||||
@@ -16,6 +16,34 @@
|
||||
<section class="carrier-view-top">
|
||||
<h3 class="carrier-view-block-title">承运商信息</h3>
|
||||
<ViewKvTable v-if="carrier" :fields="carrierViewFields" mode="triple" />
|
||||
|
||||
<h3 class="carrier-view-block-title carrier-view-attachment-title">资质附件</h3>
|
||||
<div v-if="carrier" class="carrier-view-attachments form-grid-2">
|
||||
<div class="carrier-view-attachment-item">
|
||||
<div class="carrier-view-attachment-label">营业执照</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.businessLicenseUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/license"
|
||||
/>
|
||||
</div>
|
||||
<div class="carrier-view-attachment-item">
|
||||
<div class="carrier-view-attachment-label">道路经营许可证</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.roadTransportLicenseUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/permit"
|
||||
/>
|
||||
</div>
|
||||
<div class="carrier-view-attachment-item form-col-span-2">
|
||||
<div class="carrier-view-attachment-label">运输协议</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.transportAgreementUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/agreement"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="carrier-view-bottom">
|
||||
@@ -50,6 +78,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import { carrierApi, vehicleApi, type Carrier } from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlVehicleViewPanel from '@/components/business/pl-vehicle-view-panel/index.vue'
|
||||
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
@@ -58,7 +87,12 @@ import {
|
||||
type VehicleViewBundle
|
||||
} from '@/composables/usePlVehicleViewDetail'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { getPlCarrierStatusLabel, getPlCarrierStatusTagType } from '@/utils/pl-display'
|
||||
import {
|
||||
getPlCarrierAuditStatusLabel,
|
||||
getPlCarrierAuditStatusTagType,
|
||||
getPlCarrierStatusLabel,
|
||||
getPlCarrierStatusTagType
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
@@ -92,7 +126,29 @@ const carrierViewFields = computed<ViewField[]>(() => {
|
||||
value: getPlCarrierStatusLabel(c.status),
|
||||
tagType: getPlCarrierStatusTagType(c.status)
|
||||
},
|
||||
{
|
||||
label: '审核状态',
|
||||
value: getPlCarrierAuditStatusLabel(c.auditStatus),
|
||||
tagType: getPlCarrierAuditStatusTagType(c.auditStatus)
|
||||
},
|
||||
{
|
||||
label: '审核时间',
|
||||
value: c.auditTime ? formatDateTime(c.auditTime) : '—'
|
||||
},
|
||||
{
|
||||
label: '协议有效期',
|
||||
value: c.transportAgreementExpired
|
||||
? '协议已过期'
|
||||
: c.transportAgreementExpireDate || '—',
|
||||
tagType: c.transportAgreementExpired ? 'danger' : undefined
|
||||
},
|
||||
{
|
||||
label: '资质状态',
|
||||
value: c.qualificationComplete ? '已齐全' : '资质不全',
|
||||
tagType: c.qualificationComplete ? 'success' : 'warning'
|
||||
},
|
||||
{ label: '创建时间', value: formatDateTime(c.createTime) },
|
||||
{ label: '审核备注', value: c.auditRemark || '—', span: 'full' },
|
||||
{ label: '备注', value: c.remark || '—', span: 'full' }
|
||||
]
|
||||
})
|
||||
@@ -155,6 +211,22 @@ function handleClosed() {
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.carrier-view-attachment-title {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.carrier-view-attachments {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.carrier-view-attachment-label {
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.carrier-view-empty {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -78,9 +78,6 @@
|
||||
@change="freightCalc.markFreightAmountTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否开票">
|
||||
<el-switch v-model="formData.hasInvoice" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
@@ -146,8 +143,7 @@ const productRows = ref<PlanFormProductRow[]>([])
|
||||
const formData = reactive({
|
||||
hasFreight: 0,
|
||||
distanceKm: null as number | null,
|
||||
freightType: 'by_weight',
|
||||
hasInvoice: 0
|
||||
freightType: 'by_weight'
|
||||
})
|
||||
|
||||
const hasFreightRef = computed(() => formData.hasFreight)
|
||||
@@ -205,8 +201,7 @@ function resetState() {
|
||||
Object.assign(formData, {
|
||||
hasFreight: 0,
|
||||
distanceKm: null,
|
||||
freightType: 'by_weight',
|
||||
hasInvoice: 0
|
||||
freightType: 'by_weight'
|
||||
})
|
||||
freightCalc.resetFreightCalc()
|
||||
}
|
||||
@@ -227,7 +222,6 @@ async function applyFreightPrefill(data: Plan) {
|
||||
const transport = pickBxData<Transport>(transportRes)
|
||||
formData.distanceKm = transport.distanceKm != null ? Number(transport.distanceKm) : null
|
||||
formData.freightType = transport.freightType || 'by_weight'
|
||||
formData.hasInvoice = transport.hasInvoice ?? 0
|
||||
|
||||
if (transport.transportQty != null) {
|
||||
freightCalc.transportQty.value = Number(transport.transportQty)
|
||||
@@ -311,8 +305,7 @@ async function handleSubmit() {
|
||||
transportQty:
|
||||
formData.freightType === 'by_weight'
|
||||
? freightCalc.transportQty.value ?? undefined
|
||||
: undefined,
|
||||
hasInvoice: formData.hasInvoice
|
||||
: undefined
|
||||
}
|
||||
: {})
|
||||
}
|
||||
|
||||
@@ -65,6 +65,14 @@
|
||||
|
||||
<section v-else class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">{{ isRearrange ? '调整运输' : '运输安排' }}</h3>
|
||||
<el-alert
|
||||
v-if="rearrangeBlocked"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="rearrangeBlockedMessage"
|
||||
class="pl-arrange-block-alert"
|
||||
/>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
|
||||
<h4 class="pl-plan-subtitle">车辆信息</h4>
|
||||
<div class="form-grid-4">
|
||||
@@ -100,9 +108,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运输公司">
|
||||
<el-input v-if="isSelfVehicle" :model-value="SELF_CARRIER_NAME" readonly />
|
||||
<el-select
|
||||
v-else
|
||||
v-model="formData.carrierId"
|
||||
placeholder="请选择运输公司"
|
||||
filterable
|
||||
@@ -230,9 +236,6 @@
|
||||
@change="freightCalc.markFreightAmountTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否开票">
|
||||
<el-switch v-model="formData.hasInvoice" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<h4 class="pl-plan-subtitle">安排项</h4>
|
||||
@@ -346,7 +349,7 @@
|
||||
<el-button @click="innerVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-button type="primary" :loading="submitLoading" :disabled="rearrangeBlocked" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>{{ isRearrange ? '确认调整' : '确认安排' }}
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -384,8 +387,6 @@ import { isTonProductUnit } from '@/utils/crm-product-display'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { pickBxData, pickBxList } from '@/utils/http/bx'
|
||||
|
||||
const SELF_CARRIER_NAME = '安庆市山峡化工有限公司'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -440,6 +441,8 @@ const vehicleBundle = ref<VehicleViewBundle | null>(null)
|
||||
const vehicleViewVisible = ref(false)
|
||||
const hasHistoricalDibang = ref(false)
|
||||
const hasHistoricalSiteLinks = ref(false)
|
||||
const rearrangeBlocked = ref(false)
|
||||
const rearrangeBlockedMessage = ref('')
|
||||
|
||||
const formData = reactive({
|
||||
vehicleId: '',
|
||||
@@ -454,7 +457,6 @@ const formData = reactive({
|
||||
hasFreight: 0,
|
||||
distanceKm: null as number | null,
|
||||
freightType: 'by_weight',
|
||||
hasInvoice: 0,
|
||||
remark: '',
|
||||
generateDibangPlan: false
|
||||
})
|
||||
@@ -523,10 +525,8 @@ const freightCalc = usePlFreightCalc({
|
||||
})
|
||||
|
||||
const selectedVehicle = computed(() => vehicleList.value.find(item => item.id === formData.vehicleId))
|
||||
const isSelfVehicle = computed(() => selectedVehicle.value?.vehicleType === 'self')
|
||||
|
||||
const summaryCarrierName = computed(() => {
|
||||
if (isSelfVehicle.value) return SELF_CARRIER_NAME
|
||||
const carrier = carrierList.value.find(item => item.id === formData.carrierId)
|
||||
return carrier?.carrierName || vehicleBundle.value?.detail?.carrierName || ''
|
||||
})
|
||||
@@ -565,12 +565,6 @@ function formatDriverLabel(d: VehiclePersonnelRow) {
|
||||
return `${d.personnelName}${phone}`
|
||||
}
|
||||
|
||||
function resolveSelfCarrierId() {
|
||||
if (!Array.isArray(carrierList.value)) return
|
||||
const matched = carrierList.value.find(item => item.carrierName === SELF_CARRIER_NAME)
|
||||
formData.carrierId = matched?.id || formData.carrierId || ''
|
||||
}
|
||||
|
||||
function applyVehiclePersonnel(personnelList: VehiclePersonnelRow[], preferredDriverId?: string) {
|
||||
vehicleDriverList.value = personnelList.filter(p => p.role === 'driver' && p.personnelId)
|
||||
if (preferredDriverId && vehicleDriverList.value.some(d => d.personnelId === preferredDriverId)) {
|
||||
@@ -632,7 +626,6 @@ async function applyPrefillFromPlan(data: Plan) {
|
||||
formData.remark = transport.remark || formData.remark
|
||||
formData.distanceKm = transport.distanceKm != null ? Number(transport.distanceKm) : null
|
||||
formData.freightType = transport.freightType || 'by_weight'
|
||||
formData.hasInvoice = transport.hasInvoice ?? 0
|
||||
prefilledDriverId.value = transport.driverId || prefilledDriverId.value
|
||||
|
||||
if (transport.transportQty != null) {
|
||||
@@ -662,6 +655,9 @@ function resetState() {
|
||||
vehicleBundle.value = null
|
||||
vehicleViewVisible.value = false
|
||||
hasHistoricalDibang.value = false
|
||||
hasHistoricalSiteLinks.value = false
|
||||
rearrangeBlocked.value = false
|
||||
rearrangeBlockedMessage.value = ''
|
||||
Object.assign(formData, {
|
||||
vehicleId: '',
|
||||
driverId: '',
|
||||
@@ -675,7 +671,6 @@ function resetState() {
|
||||
hasFreight: 0,
|
||||
distanceKm: null,
|
||||
freightType: 'by_weight',
|
||||
hasInvoice: 0,
|
||||
remark: '',
|
||||
generateDibangPlan: false
|
||||
})
|
||||
@@ -685,13 +680,47 @@ function resetState() {
|
||||
|
||||
async function loadMasterData() {
|
||||
const [vehicleRes, carrierRes] = await Promise.all([
|
||||
vehicleApi.list({ vehicleStatus: 'available' }),
|
||||
vehicleApi.list({ vehicleStatus: 'available', excludeIneligibleCarrier: true }),
|
||||
carrierApi.list({ status: '1' })
|
||||
])
|
||||
vehicleList.value = pickBxList<Vehicle>(vehicleRes)
|
||||
carrierList.value = pickBxList<Carrier>(carrierRes)
|
||||
}
|
||||
|
||||
async function updateRearrangeBlocked() {
|
||||
rearrangeBlocked.value = false
|
||||
rearrangeBlockedMessage.value = ''
|
||||
if (!isRearrange.value) return
|
||||
|
||||
const detail = vehicleBundle.value?.detail
|
||||
const carrierId = detail?.carrierId || formData.carrierId || selectedVehicle.value?.carrierId
|
||||
if (!carrierId) return
|
||||
|
||||
let carrier = carrierList.value.find(c => c.id === carrierId)
|
||||
if (!carrier) {
|
||||
try {
|
||||
const res = await carrierApi.get(carrierId)
|
||||
carrier = pickBxData<Carrier>(res)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (!carrier?.qualificationComplete) {
|
||||
rearrangeBlocked.value = true
|
||||
rearrangeBlockedMessage.value = '该承运商资质附件不完整,请先补全后再调整运输'
|
||||
return
|
||||
}
|
||||
if (carrier.auditStatus !== 'approved') {
|
||||
rearrangeBlocked.value = true
|
||||
rearrangeBlockedMessage.value = '该承运商尚未审核通过,请先完成审核后再调整运输'
|
||||
return
|
||||
}
|
||||
if (carrier.transportAgreementExpired) {
|
||||
rearrangeBlocked.value = true
|
||||
rearrangeBlockedMessage.value = '该承运商运输协议已过期,请先更新协议后再调整运输'
|
||||
}
|
||||
}
|
||||
|
||||
async function handleVehicleChange(vehicleId: string, preferredDriverId?: string) {
|
||||
if (!vehicleId) {
|
||||
formData.driverId = ''
|
||||
@@ -704,19 +733,20 @@ async function handleVehicleChange(vehicleId: string, preferredDriverId?: string
|
||||
const vehicle = Array.isArray(vehicleList.value)
|
||||
? vehicleList.value.find(v => v.id === vehicleId)
|
||||
: undefined
|
||||
if (vehicle?.vehicleType === 'self') {
|
||||
resolveSelfCarrierId()
|
||||
} else if (vehicle?.carrierId) {
|
||||
if (vehicle?.carrierId) {
|
||||
formData.carrierId = vehicle.carrierId
|
||||
} else {
|
||||
formData.carrierId = ''
|
||||
}
|
||||
|
||||
vehicleLoading.value = true
|
||||
try {
|
||||
vehicleBundle.value = await loadVehicleViewBundle(vehicleId)
|
||||
if (vehicleBundle.value.detail?.vehicleType === 'self') {
|
||||
resolveSelfCarrierId()
|
||||
if (vehicleBundle.value.detail?.carrierId) {
|
||||
formData.carrierId = vehicleBundle.value.detail.carrierId
|
||||
}
|
||||
applyVehiclePersonnel(vehicleBundle.value.personnelList || [], preferredDriverId)
|
||||
await updateRearrangeBlocked()
|
||||
} finally {
|
||||
vehicleLoading.value = false
|
||||
}
|
||||
@@ -753,6 +783,10 @@ function handleClosed() {
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (rearrangeBlocked.value) {
|
||||
ElMessage.warning(rearrangeBlockedMessage.value || '当前承运商不可调整运输')
|
||||
return
|
||||
}
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
@@ -815,7 +849,6 @@ async function handleSubmit() {
|
||||
formData.hasFreight === 1
|
||||
? freightCalc.calculatedFreight.value || freightCalc.freightAmount.value || undefined
|
||||
: undefined,
|
||||
hasInvoice: formData.hasInvoice,
|
||||
remark: formData.remark || undefined,
|
||||
generateDibangPlan: formData.generateDibangPlan
|
||||
}
|
||||
@@ -858,6 +891,10 @@ async function handleSubmit() {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.pl-arrange-block-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pl-plan-subtitle {
|
||||
margin: 16px 0 10px;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -50,15 +50,6 @@
|
||||
<span v-else>{{ row.actualQty ?? '—' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="含税单价(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyDisplay(row.taxPrice) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="含税总价(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyDisplay(row.taxSubtotal) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税费(元)" min-width="100" align="right">
|
||||
<template #default="{ row }">{{ formatMoneyDisplay(row.taxAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="packageSpec" label="包装规格" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="送货方式" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
@@ -89,7 +80,6 @@ import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { getDeliveryMethodLabel } from '@/composables/usePlContractToPlan'
|
||||
import PlProductNameHazardCell from '@/components/business/pl-product-name-hazard-cell/index.vue'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
defineProps<{
|
||||
products: PlanFormProductRow[]
|
||||
|
||||
@@ -100,9 +100,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="运输公司">
|
||||
<el-input v-if="isSelfVehicle" :model-value="SELF_CARRIER_NAME" readonly />
|
||||
<el-select
|
||||
v-else
|
||||
v-model="formData.carrierId"
|
||||
placeholder="请选择运输公司"
|
||||
filterable
|
||||
@@ -222,9 +220,6 @@
|
||||
@change="freightCalc.markFreightAmountTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否开票">
|
||||
<el-switch v-model="formData.hasInvoice" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -258,8 +253,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ContractSelector v-model:visible="contractSelectorVisible" @confirm="handleContractConfirm" />
|
||||
<ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" />
|
||||
<ContractSelector v-model:visible="contractSelectorVisible" hide-amount @confirm="handleContractConfirm" />
|
||||
<ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" hide-product-price />
|
||||
<PlVehicleViewDialog
|
||||
v-model:visible="vehicleViewVisible"
|
||||
:vehicle-id="formData.vehicleId"
|
||||
@@ -296,8 +291,6 @@ const ContractDetailDialog = defineAsyncComponent(
|
||||
() => import('@/views/crm/contract/components/ContractDetailDialog.vue')
|
||||
)
|
||||
|
||||
const SELF_CARRIER_NAME = '安庆市山峡化工有限公司'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
planId?: string
|
||||
@@ -388,7 +381,6 @@ const formData = reactive({
|
||||
carrierId: '',
|
||||
distanceKm: null as number | null,
|
||||
freightType: 'by_weight',
|
||||
hasInvoice: 0,
|
||||
remark: '',
|
||||
products: [] as PlanFormProductRow[]
|
||||
})
|
||||
@@ -414,13 +406,8 @@ const freightUnitLabel = computed(() => {
|
||||
})
|
||||
|
||||
const selectedVehicle = computed(() => vehicleList.value.find(item => item.id === formData.vehicleId))
|
||||
const isSelfVehicle = computed(
|
||||
() =>
|
||||
selectedVehicle.value?.vehicleType === 'self' || vehicleBundle.value?.detail?.vehicleType === 'self'
|
||||
)
|
||||
|
||||
const summaryCarrierName = computed(() => {
|
||||
if (isSelfVehicle.value) return SELF_CARRIER_NAME
|
||||
const carrier = carrierList.value.find(item => item.id === formData.carrierId)
|
||||
if (carrier?.carrierName) return carrier.carrierName
|
||||
return vehicleBundle.value?.detail?.carrierName || ''
|
||||
@@ -445,12 +432,6 @@ function buildContractBasicFields(contract: ContractVO): ViewField[] {
|
||||
value: settlementMethod ? dictStore.getLabel('crm_settlement_method', settlementMethod) : '—',
|
||||
tagType: settlementMethod ? dictStore.getTagType('crm_settlement_method', settlementMethod) : undefined,
|
||||
long: false
|
||||
},
|
||||
{
|
||||
label: '合同金额(元)',
|
||||
value: formatMoneyDisplay(contract.amount),
|
||||
money: true,
|
||||
long: false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -470,11 +451,6 @@ const contractProductRows = computed<PlanFormProductRow[]>(() =>
|
||||
mapContractProducts(contractSnapshot.value?.products)
|
||||
)
|
||||
|
||||
function resolveSelfCarrierId() {
|
||||
const matched = carrierList.value.find(item => item.carrierName === SELF_CARRIER_NAME)
|
||||
formData.carrierId = matched?.id || ''
|
||||
}
|
||||
|
||||
function enrichProductsFromContract(products: PlanFormProductRow[], contract?: ContractVO | null) {
|
||||
if (!contract?.products?.length) return products
|
||||
const contractMap = new Map(
|
||||
@@ -556,8 +532,7 @@ function buildTransportBlock(options?: { allowClear?: boolean }) {
|
||||
if (!options?.allowClear) return undefined
|
||||
return {
|
||||
vehicleId: '',
|
||||
freightType: formData.freightType,
|
||||
hasInvoice: formData.hasInvoice
|
||||
freightType: formData.freightType
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -570,8 +545,7 @@ function buildTransportBlock(options?: { allowClear?: boolean }) {
|
||||
formData.freightType === 'by_weight' ? freightCalc.transportQty.value ?? undefined : undefined,
|
||||
freightPrice:
|
||||
formData.freightType === 'by_weight' ? freightCalc.freightPrice.value ?? undefined : undefined,
|
||||
freightAmount: freightCalc.freightAmount.value ?? undefined,
|
||||
hasInvoice: formData.hasInvoice
|
||||
freightAmount: freightCalc.freightAmount.value ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,7 +596,6 @@ async function restoreFreightFromTransport(plan: Plan) {
|
||||
const transport = (transportRes.data ?? transportRes) as {
|
||||
distanceKm?: number
|
||||
freightType?: string
|
||||
hasInvoice?: number
|
||||
freightPrice?: number
|
||||
transportQty?: number
|
||||
freightAmount?: number
|
||||
@@ -631,7 +604,6 @@ async function restoreFreightFromTransport(plan: Plan) {
|
||||
}
|
||||
formData.distanceKm = transport.distanceKm != null ? Number(transport.distanceKm) : null
|
||||
formData.freightType = transport.freightType || 'by_weight'
|
||||
formData.hasInvoice = transport.hasInvoice ?? 0
|
||||
if (transport.carrierId) {
|
||||
formData.carrierId = transport.carrierId
|
||||
}
|
||||
@@ -689,7 +661,6 @@ function resetForm() {
|
||||
carrierId: '',
|
||||
distanceKm: null,
|
||||
freightType: 'by_weight',
|
||||
hasInvoice: 0,
|
||||
remark: '',
|
||||
products: []
|
||||
})
|
||||
@@ -821,7 +792,7 @@ function handleClosed() {
|
||||
async function loadMasterData() {
|
||||
try {
|
||||
const [vehicleRes, carrierRes] = await Promise.all([
|
||||
vehicleApi.list({ vehicleStatus: 'available' }),
|
||||
vehicleApi.list({ vehicleStatus: 'available', excludeIneligibleCarrier: true }),
|
||||
carrierApi.list({ status: '1' })
|
||||
])
|
||||
vehicleList.value = (vehicleRes.data ?? vehicleRes) || []
|
||||
@@ -877,17 +848,17 @@ async function handleVehicleChange(vehicleId: string) {
|
||||
}
|
||||
|
||||
const vehicle = vehicleList.value.find(item => item.id === vehicleId)
|
||||
if (vehicle?.vehicleType === 'self') {
|
||||
resolveSelfCarrierId()
|
||||
} else if (vehicle?.carrierId) {
|
||||
if (vehicle?.carrierId) {
|
||||
formData.carrierId = vehicle.carrierId
|
||||
} else {
|
||||
formData.carrierId = ''
|
||||
}
|
||||
|
||||
vehicleLoading.value = true
|
||||
try {
|
||||
vehicleBundle.value = await loadVehicleViewBundle(vehicleId)
|
||||
if (vehicleBundle.value.detail?.vehicleType === 'self') {
|
||||
resolveSelfCarrierId()
|
||||
if (vehicleBundle.value.detail?.carrierId) {
|
||||
formData.carrierId = vehicleBundle.value.detail.carrierId
|
||||
}
|
||||
resolveDefaultDriver(vehicleBundle.value.personnelList || [])
|
||||
} finally {
|
||||
|
||||
@@ -341,7 +341,7 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" />
|
||||
<ContractDetailDialog v-if="contractDetailDialogActive" ref="contractDetailRef" hide-product-price />
|
||||
<PlVehicleViewDialog
|
||||
v-model:visible="vehicleViewVisible"
|
||||
:vehicle-id="plan?.vehicleId"
|
||||
@@ -376,6 +376,7 @@
|
||||
import {
|
||||
formatPlanProductListText,
|
||||
getPlFreightTypeLabel,
|
||||
getPlHasInvoiceLabelFromSettlement,
|
||||
getPlPlanCompanyLabel,
|
||||
getPlPlanTypeLabel,
|
||||
getPlYesNoLabel,
|
||||
@@ -388,8 +389,6 @@
|
||||
() => import('@/views/crm/contract/components/ContractDetailDialog.vue')
|
||||
)
|
||||
|
||||
const SELF_CARRIER_NAME = '安庆市山峡化工有限公司'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -409,6 +408,7 @@
|
||||
plan,
|
||||
products,
|
||||
transport,
|
||||
freightSettlement,
|
||||
vehicleBundle,
|
||||
contractSnapshot,
|
||||
dibangRows,
|
||||
@@ -516,7 +516,6 @@
|
||||
if (transport.value?.carrierName) return transport.value.carrierName
|
||||
const vehicle = vehicleBundle.value?.detail
|
||||
if (vehicle?.carrierName) return vehicle.carrierName
|
||||
if (vehicle?.vehicleType === 'self') return SELF_CARRIER_NAME
|
||||
return ''
|
||||
})
|
||||
|
||||
@@ -608,7 +607,7 @@
|
||||
}
|
||||
fields.push(
|
||||
{ label: '运费金额(元)', value: displayFreightAmount.value, money: true },
|
||||
{ label: '是否开票', value: getPlYesNoLabel(transport.value?.hasInvoice) }
|
||||
{ label: '是否开票', value: getPlHasInvoiceLabelFromSettlement(freightSettlement.value) }
|
||||
)
|
||||
return fields
|
||||
})
|
||||
@@ -637,8 +636,7 @@
|
||||
value: c.startDate && c.endDate ? `${c.startDate} ~ ${c.endDate}` : '—',
|
||||
long: false
|
||||
},
|
||||
{ label: '结算方式', value: settlementLabel },
|
||||
{ label: '合同金额(元)', value: formatMoneyDisplay(c.amount), money: true }
|
||||
{ label: '结算方式', value: settlementLabel }
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="vehicle-cert-part-tabs">
|
||||
<el-tabs v-if="hasTrailer" v-model="activePart" class="vehicle-cert-part-tabs__tabs">
|
||||
<el-tab-pane :label="headTabLabel" name="head">
|
||||
<PlCertFormList
|
||||
ref="headCertFormRef"
|
||||
v-model="headCertList"
|
||||
:title="headTabLabel"
|
||||
:upload-folder="uploadFolder"
|
||||
:cert-name-options="certNameOptions"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="trailerTabLabel" name="trailer">
|
||||
<PlCertFormList
|
||||
ref="trailerCertFormRef"
|
||||
v-model="trailerCertList"
|
||||
:title="trailerTabLabel"
|
||||
:upload-folder="uploadFolder"
|
||||
:cert-name-options="certNameOptions"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<PlCertFormList
|
||||
v-else
|
||||
ref="headCertFormRef"
|
||||
v-model="headCertList"
|
||||
:title="title"
|
||||
:upload-folder="uploadFolder"
|
||||
:cert-name-options="certNameOptions"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PlCertFormList from '@/components/business/pl-cert-form-list/index.vue'
|
||||
import type { CertRow } from '@/api/pl'
|
||||
import {
|
||||
PL_VEHICLE_CERT_PART_HEAD,
|
||||
PL_VEHICLE_CERT_PART_TRAILER,
|
||||
mergeVehicleCertsByPart,
|
||||
splitVehicleCertsByPart
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
|
||||
type CertRowWithKey = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
licensePlate?: string
|
||||
trailerNo?: string
|
||||
uploadFolder: string
|
||||
certNameOptions?: string[]
|
||||
title?: string
|
||||
}>(),
|
||||
{
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
certNameOptions: () => [],
|
||||
title: '车辆证书'
|
||||
}
|
||||
)
|
||||
|
||||
const certList = defineModel<CertRowWithKey[]>('modelValue', { default: () => [] })
|
||||
|
||||
const activePart = ref(PL_VEHICLE_CERT_PART_HEAD)
|
||||
const headCertFormRef = ref<CertFormListRef>()
|
||||
const trailerCertFormRef = ref<CertFormListRef>()
|
||||
|
||||
const hasTrailer = computed(() => Boolean(props.trailerNo?.trim()))
|
||||
|
||||
const headTabLabel = computed(() => props.licensePlate?.trim() || '主车')
|
||||
const trailerTabLabel = computed(() => props.trailerNo?.trim() || '挂车')
|
||||
|
||||
const headCertList = computed<CertRowWithKey[]>({
|
||||
get() {
|
||||
return splitVehicleCertsByPart(certList.value).headCerts
|
||||
},
|
||||
set(value) {
|
||||
syncCertList(value, trailerCertList.value)
|
||||
}
|
||||
})
|
||||
|
||||
const trailerCertList = computed<CertRowWithKey[]>({
|
||||
get() {
|
||||
return splitVehicleCertsByPart(certList.value).trailerCerts
|
||||
},
|
||||
set(value) {
|
||||
syncCertList(headCertList.value, value)
|
||||
}
|
||||
})
|
||||
|
||||
function syncCertList(headCerts: CertRowWithKey[], trailerCerts: CertRowWithKey[]) {
|
||||
certList.value = mergeVehicleCertsByPart(headCerts, trailerCerts, hasTrailer.value)
|
||||
}
|
||||
|
||||
watch(hasTrailer, value => {
|
||||
if (!value) {
|
||||
activePart.value = PL_VEHICLE_CERT_PART_HEAD
|
||||
certList.value = mergeVehicleCertsByPart(
|
||||
splitVehicleCertsByPart(certList.value).headCerts,
|
||||
[],
|
||||
false
|
||||
)
|
||||
trailerCertFormRef.value?.clearUploadRefs()
|
||||
return
|
||||
}
|
||||
if (activePart.value !== PL_VEHICLE_CERT_PART_TRAILER) {
|
||||
activePart.value = PL_VEHICLE_CERT_PART_HEAD
|
||||
}
|
||||
})
|
||||
|
||||
function hasUploading() {
|
||||
return Boolean(
|
||||
headCertFormRef.value?.hasUploading?.() || trailerCertFormRef.value?.hasUploading?.()
|
||||
)
|
||||
}
|
||||
|
||||
function clearUploadRefs() {
|
||||
headCertFormRef.value?.clearUploadRefs()
|
||||
trailerCertFormRef.value?.clearUploadRefs()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
hasUploading,
|
||||
clearUploadRefs
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.vehicle-cert-part-tabs {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.vehicle-cert-part-tabs__tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.cert-form-section) {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="vehicle-cert-part-view">
|
||||
<el-tabs v-if="hasTrailer" v-model="activePart" class="vehicle-cert-part-view__tabs">
|
||||
<el-tab-pane :label="headTabLabel" name="head">
|
||||
<PlCertViewList :cert-list="headCertList" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="trailerTabLabel" name="trailer">
|
||||
<PlCertViewList :cert-list="trailerCertList" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<PlCertViewList v-else :cert-list="headCertList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PlCertViewList from '@/components/business/pl-cert-view-list/index.vue'
|
||||
import type { CertRead } from '@/api/pl'
|
||||
import {
|
||||
PL_VEHICLE_CERT_PART_HEAD,
|
||||
splitVehicleCertsByPart
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
certList?: CertRead[]
|
||||
licensePlate?: string
|
||||
trailerNo?: string
|
||||
}>(),
|
||||
{
|
||||
certList: () => [],
|
||||
licensePlate: '',
|
||||
trailerNo: ''
|
||||
}
|
||||
)
|
||||
|
||||
const activePart = ref(PL_VEHICLE_CERT_PART_HEAD)
|
||||
|
||||
const hasTrailer = computed(() => Boolean(props.trailerNo?.trim()))
|
||||
const headTabLabel = computed(() => props.licensePlate?.trim() || '主车')
|
||||
const trailerTabLabel = computed(() => props.trailerNo?.trim() || '挂车')
|
||||
|
||||
const headCertList = computed(() => splitVehicleCertsByPart(props.certList).headCerts)
|
||||
const trailerCertList = computed(() => splitVehicleCertsByPart(props.certList).trailerCerts)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.vehicle-cert-part-view__tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTab" class="vehicle-detail-tabs">
|
||||
<el-tab-pane label="车辆证书" name="vehicle-cert">
|
||||
<PlCertViewList :cert-list="certList" />
|
||||
<PlVehicleCertPartView
|
||||
:cert-list="certList"
|
||||
:license-plate="licensePlate"
|
||||
:trailer-no="trailerNo"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
v-for="(person, index) in personnelList"
|
||||
@@ -15,7 +19,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PlCertViewList from '@/components/business/pl-cert-view-list/index.vue'
|
||||
import PlVehicleCertPartView from '@/components/business/pl-vehicle-cert-part-view/index.vue'
|
||||
import PersonnelViewPanel from '@/components/business/pl-vehicle-personnel-view-list/PersonnelViewPanel.vue'
|
||||
import type { CertRead } from '@/api/pl'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
@@ -23,10 +27,14 @@ import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-p
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
certList?: CertRead[]
|
||||
licensePlate?: string
|
||||
trailerNo?: string
|
||||
personnelList?: VehiclePersonnelViewRow[]
|
||||
}>(),
|
||||
{
|
||||
certList: () => [],
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
personnelList: () => []
|
||||
}
|
||||
)
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
import {
|
||||
planApi,
|
||||
transportApi,
|
||||
freightApi,
|
||||
type FreightSettlementBrief,
|
||||
type Plan,
|
||||
type PlanProduct,
|
||||
type Transport,
|
||||
@@ -124,6 +126,7 @@ export function usePlPlanViewDetail() {
|
||||
const plan = ref<Plan | null>(null)
|
||||
const products = ref<PlanProduct[]>([])
|
||||
const transport = ref<Transport | null>(null)
|
||||
const freightSettlement = ref<FreightSettlementBrief | null>(null)
|
||||
const vehicleBundle = ref<VehicleViewBundle | null>(null)
|
||||
const contractSnapshot = ref<ContractVO | null>(null)
|
||||
const dibangRows = ref<PlPlanTransportDibangRow[]>([])
|
||||
@@ -183,6 +186,7 @@ export function usePlPlanViewDetail() {
|
||||
plan.value = null
|
||||
products.value = []
|
||||
transport.value = null
|
||||
freightSettlement.value = null
|
||||
vehicleBundle.value = null
|
||||
contractSnapshot.value = null
|
||||
dibangRows.value = []
|
||||
@@ -219,6 +223,16 @@ export function usePlPlanViewDetail() {
|
||||
transport.value = null
|
||||
})
|
||||
)
|
||||
detailTasks.push(
|
||||
freightApi
|
||||
.getByTransport(data.transportId)
|
||||
.then((settlementRes) => {
|
||||
freightSettlement.value = pickBxData<FreightSettlementBrief>(settlementRes)
|
||||
})
|
||||
.catch(() => {
|
||||
freightSettlement.value = null
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (data?.vehicleId) {
|
||||
@@ -249,6 +263,7 @@ export function usePlPlanViewDetail() {
|
||||
plan.value = null
|
||||
products.value = []
|
||||
transport.value = null
|
||||
freightSettlement.value = null
|
||||
vehicleBundle.value = null
|
||||
contractSnapshot.value = null
|
||||
dibangRows.value = []
|
||||
@@ -260,6 +275,7 @@ export function usePlPlanViewDetail() {
|
||||
plan,
|
||||
products,
|
||||
transport,
|
||||
freightSettlement,
|
||||
vehicleBundle,
|
||||
contractSnapshot,
|
||||
dibangRows,
|
||||
|
||||
@@ -123,6 +123,48 @@ export const plRoutes: AppRouteRecord[] = [
|
||||
component: '/pl/freight-disburse/index',
|
||||
meta: { title: '运费财务发放', keepAlive: true, menuCode: 'pl:freight-disburse:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL_VEH_RECHARGE_APPLY',
|
||||
name: 'PlVehicleRechargeApply',
|
||||
path: 'vehicle-recharge-apply',
|
||||
component: '/pl/vehicle-recharge-apply/index',
|
||||
meta: { title: 'ETC加油申请', keepAlive: true, menuCode: 'pl:vehicle-recharge-apply:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL_VEH_RECHARGE_RECORD',
|
||||
name: 'PlVehicleRechargeRecord',
|
||||
path: 'vehicle-recharge-record',
|
||||
component: '/pl/vehicle-recharge-record/index',
|
||||
meta: { title: 'ETC加油发放登记', keepAlive: true, menuCode: 'pl:vehicle-recharge-record:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL_VEH_MAINT_APPLY',
|
||||
name: 'PlVehicleMaintenanceApply',
|
||||
path: 'vehicle-maintenance-apply',
|
||||
component: '/pl/vehicle-maintenance-apply/index',
|
||||
meta: { title: '车辆维保申请', keepAlive: true, menuCode: 'pl:vehicle-maintenance-apply:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL_VEH_MAINT_PROCESS',
|
||||
name: 'PlVehicleMaintenanceProcess',
|
||||
path: 'vehicle-maintenance-process',
|
||||
component: '/pl/vehicle-maintenance-process/index',
|
||||
meta: { title: '车辆维保处理登记', keepAlive: true, menuCode: 'pl:vehicle-maintenance-process:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL_VEH_UREA_APPLY',
|
||||
name: 'PlVehicleUreaApply',
|
||||
path: 'vehicle-urea-apply',
|
||||
component: '/pl/vehicle-urea-apply/index',
|
||||
meta: { title: '车辆尿素申请', keepAlive: true, menuCode: 'pl:vehicle-urea-apply:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL_VEH_UREA_DISBURSE',
|
||||
name: 'PlVehicleUreaDisburse',
|
||||
path: 'vehicle-urea-disburse',
|
||||
component: '/pl/vehicle-urea-disburse/index',
|
||||
meta: { title: '车辆尿素发放登记', keepAlive: true, menuCode: 'pl:vehicle-urea-disburse:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL00000000000000000000000000040',
|
||||
name: 'PlInventory',
|
||||
|
||||
@@ -654,6 +654,17 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
menuCode: 'system:post:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'SYS_OPERLOG',
|
||||
name: 'SystemOperlog',
|
||||
path: 'operlog',
|
||||
component: '/system/operlog/index',
|
||||
meta: {
|
||||
title: '操作日志',
|
||||
keepAlive: true,
|
||||
menuCode: 'system:operlog:list'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '10000000000000000000000000000008',
|
||||
name: 'UserDetail',
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
export const CREATOR_DELETE_TIP = '仅创建人可删除'
|
||||
|
||||
/** 当前登录用户 ID(UUID 字符串) */
|
||||
export function getCurrentUserId(): string {
|
||||
const info = useUserStore().info as Record<string, unknown>
|
||||
const id = info?.id ?? info?.userId ?? info?.user_id
|
||||
if (id === undefined || id === null || id === '') return ''
|
||||
return String(id)
|
||||
}
|
||||
|
||||
/** 是否 super_admin */
|
||||
export function isSuperAdmin(): boolean {
|
||||
const info = useUserStore().info as Record<string, unknown>
|
||||
const roles = (info?.roles as string[] | undefined) ?? []
|
||||
const role = (info?.role as string | undefined) ?? ''
|
||||
return role === 'super_admin' || roles.includes('super_admin')
|
||||
}
|
||||
|
||||
export function rowCreatorId(row: Record<string, unknown> | null | undefined): string {
|
||||
if (!row) return ''
|
||||
const creator =
|
||||
row.createBy ??
|
||||
row.create_by ??
|
||||
row.applicantId ??
|
||||
row.applicant_id
|
||||
if (creator === undefined || creator === null || creator === '') return ''
|
||||
return String(creator)
|
||||
}
|
||||
|
||||
/** 是否允许删除(仅创建人或 super_admin) */
|
||||
export function canDeleteByCreator(
|
||||
row: Record<string, unknown> | null | undefined,
|
||||
currentUserId?: string
|
||||
): boolean {
|
||||
if (!row) return false
|
||||
if (isSuperAdmin()) return true
|
||||
const uid = (currentUserId ?? getCurrentUserId()).trim()
|
||||
const creator = rowCreatorId(row)
|
||||
if (!creator) return false
|
||||
if (uid && creator === uid) return true
|
||||
const info = useUserStore().info as Record<string, unknown>
|
||||
const names = [info?.realName, info?.real_name, info?.userName, info?.username, info?.nickName]
|
||||
.filter(Boolean)
|
||||
.map(String)
|
||||
return names.includes(creator)
|
||||
}
|
||||
|
||||
/** 批量删除前过滤可删行 */
|
||||
export function filterDeletableByCreator<T extends Record<string, unknown>>(
|
||||
rows: T[],
|
||||
currentUserId?: string
|
||||
): T[] {
|
||||
return rows.filter((row) => canDeleteByCreator(row, currentUserId))
|
||||
}
|
||||
|
||||
/** 选中项是否全部可删 */
|
||||
export function allRowsDeletableByCreator(
|
||||
rows: Record<string, unknown>[],
|
||||
currentUserId?: string
|
||||
): boolean {
|
||||
if (!rows.length) return false
|
||||
return rows.every((row) => canDeleteByCreator(row, currentUserId))
|
||||
}
|
||||
@@ -32,6 +32,24 @@ export function getPlCarrierStatusTagType(status: string) {
|
||||
return status === '1' ? 'success' : 'danger'
|
||||
}
|
||||
|
||||
export function getPlCarrierAuditStatusLabel(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已驳回'
|
||||
}
|
||||
return status ? map[status] || status : '待审核'
|
||||
}
|
||||
|
||||
export function getPlCarrierAuditStatusTagType(status?: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger'
|
||||
}
|
||||
return map[status || ''] || 'info'
|
||||
}
|
||||
|
||||
export function getPlanProductSpec(product: { model?: string; packageSpec?: string }) {
|
||||
return product.model || product.packageSpec || '—'
|
||||
}
|
||||
@@ -150,6 +168,21 @@ export function getPlYesNoLabel(value?: number | boolean | null) {
|
||||
return value === 1 || value === true ? '是' : '否'
|
||||
}
|
||||
|
||||
export function getPlHasInvoiceLabelFromSettlement(settlement?: {
|
||||
invoiceNo?: string | null
|
||||
invoiceDate?: string | null
|
||||
invoiceAmount?: number | null
|
||||
invoiceAttachmentUrls?: string | null
|
||||
} | null) {
|
||||
if (!settlement) return '—'
|
||||
const hasInvoice =
|
||||
Boolean(settlement.invoiceNo?.trim()) ||
|
||||
Boolean(settlement.invoiceDate) ||
|
||||
(settlement.invoiceAmount != null && Number(settlement.invoiceAmount) > 0) ||
|
||||
Boolean(settlement.invoiceAttachmentUrls?.trim())
|
||||
return hasInvoice ? '是' : '否'
|
||||
}
|
||||
|
||||
/** 解析运输单司机回传附件(JSON 数组或逗号分隔) */
|
||||
export function parsePlFeedbackImages(raw?: string | string[] | null): string[] {
|
||||
if (!raw) return []
|
||||
@@ -169,3 +202,84 @@ export function parsePlFeedbackImages(raw?: string | string[] | null): string[]
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function getPlRechargeTypeLabel(type: string) {
|
||||
return type === 'etc' ? 'ETC' : type === 'fuel' ? '加油' : type
|
||||
}
|
||||
|
||||
export function getPlRechargeTypeTagType(type: string) {
|
||||
return type === 'etc' ? 'primary' : type === 'fuel' ? 'success' : 'info'
|
||||
}
|
||||
|
||||
export function getPlRechargeApplyStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待发放',
|
||||
disbursed: '已发放',
|
||||
cancelled: '已作废'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
export function getPlRechargeApplyStatusTagType(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
disbursed: 'success',
|
||||
cancelled: 'info'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
export const PL_GAS_STATION_PRESETS = ['中石化', '线上etc-app', '百链']
|
||||
|
||||
export const PL_MAINTENANCE_TYPE_PRESETS = ['保养', '维修', '年检']
|
||||
|
||||
export function getPlMaintenanceTypeLabel(type: string) {
|
||||
return type || '—'
|
||||
}
|
||||
|
||||
export function getPlMaintenanceTypeTagType(type: string) {
|
||||
const map: Record<string, string> = {
|
||||
保养: 'success',
|
||||
维修: 'warning',
|
||||
年检: 'primary'
|
||||
}
|
||||
return map[type] || 'info'
|
||||
}
|
||||
|
||||
export function getPlMaintenanceApplyStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
processed: '已处理',
|
||||
cancelled: '已作废'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
export function getPlMaintenanceApplyStatusTagType(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
processed: 'success',
|
||||
cancelled: 'info'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
export const PL_UREA_UNIT_PRESETS = ['吨桶']
|
||||
|
||||
export function getPlUreaApplyStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待发放',
|
||||
disbursed: '已发放',
|
||||
cancelled: '已作废'
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
export function getPlUreaApplyStatusTagType(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
disbursed: 'success',
|
||||
cancelled: 'info'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { CertRow } from '@/api/pl'
|
||||
|
||||
export const PL_VEHICLE_CERT_PART_HEAD = 'head'
|
||||
export const PL_VEHICLE_CERT_PART_TRAILER = 'trailer'
|
||||
|
||||
export const PL_VEHICLE_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
|
||||
|
||||
export const PL_VEHICLE_CERT_NAME_OPTIONS = [
|
||||
'行驶证',
|
||||
'营运证',
|
||||
'道路运输证',
|
||||
'驾驶证',
|
||||
'押运员证',
|
||||
'危化品从业资格证',
|
||||
'保险单'
|
||||
]
|
||||
|
||||
export type VehicleCertPart = typeof PL_VEHICLE_CERT_PART_HEAD | typeof PL_VEHICLE_CERT_PART_TRAILER
|
||||
|
||||
export function normalizeVehicleCertPart(part?: string | null): VehicleCertPart {
|
||||
return part === PL_VEHICLE_CERT_PART_TRAILER ? PL_VEHICLE_CERT_PART_TRAILER : PL_VEHICLE_CERT_PART_HEAD
|
||||
}
|
||||
|
||||
export function splitVehicleCertsByPart<T extends CertRow>(certList: T[] = []) {
|
||||
const headCerts: T[] = []
|
||||
const trailerCerts: T[] = []
|
||||
for (const cert of certList) {
|
||||
if (normalizeVehicleCertPart(cert.vehiclePart) === PL_VEHICLE_CERT_PART_TRAILER) {
|
||||
trailerCerts.push(cert)
|
||||
} else {
|
||||
headCerts.push(cert)
|
||||
}
|
||||
}
|
||||
return { headCerts, trailerCerts }
|
||||
}
|
||||
|
||||
export function mergeVehicleCertsByPart<T extends CertRow>(
|
||||
headCerts: T[],
|
||||
trailerCerts: T[],
|
||||
hasTrailer: boolean
|
||||
) {
|
||||
const normalizedHead = headCerts.map(cert => ({
|
||||
...cert,
|
||||
vehiclePart: PL_VEHICLE_CERT_PART_HEAD
|
||||
}))
|
||||
if (!hasTrailer) {
|
||||
return normalizedHead
|
||||
}
|
||||
const normalizedTrailer = trailerCerts.map(cert => ({
|
||||
...cert,
|
||||
vehiclePart: PL_VEHICLE_CERT_PART_TRAILER
|
||||
}))
|
||||
return [...normalizedHead, ...normalizedTrailer]
|
||||
}
|
||||
|
||||
function isCertFilled(cert?: CertRow) {
|
||||
if (!cert) return false
|
||||
return Boolean(
|
||||
cert.certName?.trim() &&
|
||||
cert.certNo?.trim() &&
|
||||
cert.expireDate &&
|
||||
cert.attachmentUrls?.trim()
|
||||
)
|
||||
}
|
||||
|
||||
export function validateVehicleRequiredCerts(options: {
|
||||
headCerts: CertRow[]
|
||||
trailerCerts: CertRow[]
|
||||
hasTrailer: boolean
|
||||
licensePlate: string
|
||||
trailerNo: string
|
||||
onError: (message: string) => void
|
||||
}) {
|
||||
const { headCerts, trailerCerts, hasTrailer, licensePlate, trailerNo, onError } = options
|
||||
const parts: Array<{ label: string; certs: CertRow[] }> = [
|
||||
{ label: licensePlate?.trim() || '主车', certs: headCerts }
|
||||
]
|
||||
if (hasTrailer) {
|
||||
parts.push({ label: trailerNo?.trim() || '挂车', certs: trailerCerts })
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
for (const certName of PL_VEHICLE_REQUIRED_CERT_NAMES) {
|
||||
const matched = part.certs.find(item => item.certName?.trim() === certName)
|
||||
if (!isCertFilled(matched)) {
|
||||
onError(`请完善「${part.label}」的${certName}(证号、有效期、附件均必填)`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const certs of [headCerts, ...(hasTrailer ? [trailerCerts] : [])]) {
|
||||
for (const cert of certs) {
|
||||
if (!cert.certName?.trim()) {
|
||||
onError('证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function filterVehicleCertsForSubmit<T extends CertRow>(
|
||||
headCerts: T[],
|
||||
trailerCerts: T[],
|
||||
hasTrailer: boolean
|
||||
) {
|
||||
return mergeVehicleCertsByPart(headCerts, trailerCerts, hasTrailer)
|
||||
}
|
||||
@@ -224,7 +224,7 @@
|
||||
<el-icon><Upload /></el-icon>提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'draft' || row.status === 'rejected'"
|
||||
v-if="(row.status === 'draft' || row.status === 'rejected') && canDeleteByCreator(row)"
|
||||
class="btn-op-item"
|
||||
:loading="loadingMap[`del_${row.id}`]"
|
||||
@click="handleDelete(row)"
|
||||
@@ -433,6 +433,7 @@ import {
|
||||
ArrowDownBold
|
||||
} from '@element-plus/icons-vue'
|
||||
import { advanceApi, paymentApi } from '@/api/bx'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
statusLabel,
|
||||
statusTagType,
|
||||
@@ -758,6 +759,10 @@ async function handleSubmit(row: any) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该申请单吗?删除后不可恢复。', '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
|
||||
@@ -124,7 +124,13 @@
|
||||
>
|
||||
<el-icon><Plus /></el-icon>新增子项
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" :loading="loadingMap.rowDelete" @click="handleDelete(row)">
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -325,6 +331,7 @@ import {
|
||||
SortDown
|
||||
} from '@element-plus/icons-vue'
|
||||
import { expenseItemApi } from '@/api/bx'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { statusLabel, statusTagType } from '@/utils/bx/format'
|
||||
import { suggestExpenseItemCode } from '@/utils/bx/expenseItemCode'
|
||||
|
||||
@@ -512,6 +519,10 @@ async function handleAddChildSubmit() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除费用项目「${row.itemName || row.itemCode}」吗?`,
|
||||
'删除确认',
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<el-icon><Upload /></el-icon>提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'draft' || row.status === 'rejected'"
|
||||
v-if="(row.status === 'draft' || row.status === 'rejected') && canDeleteByCreator(row)"
|
||||
class="btn-op-item"
|
||||
:loading="loadingMap[`del_${row.id}`]"
|
||||
@click="handleDelete(row)"
|
||||
@@ -474,6 +474,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { reimburseApi, attachmentApi } from '@/api/bx'
|
||||
import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { useExpenseItemTree, type ExpenseItem } from '@/hooks/bx/useExpenseItemTree'
|
||||
import {
|
||||
statusLabel,
|
||||
@@ -911,6 +912,10 @@ async function handleSubmit(row: any) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该报销单吗?删除后不可恢复。', '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
|
||||
@@ -64,7 +64,13 @@
|
||||
<el-button class="btn-op-item" @click="openEditDialog(row)">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" :loading="loadingMap.rowDelete" @click="deleteNode(row)">
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="deleteNode(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="moveUp(row)">
|
||||
@@ -180,6 +186,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import {
|
||||
@@ -365,6 +372,10 @@ async function submitEditNode() {
|
||||
}
|
||||
|
||||
async function deleteNode(node: Record<string, any>) {
|
||||
if (!canDeleteByCreator(node)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定要删除节点「${node.nodeName}」吗?`, '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -64,7 +64,13 @@
|
||||
<el-button class="btn-op-item" @click="openEditDialog(row)">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" :loading="loadingMap.rowDelete" @click="deleteNode(row)">
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="deleteNode(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="moveUp(row)">
|
||||
@@ -180,6 +186,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import {
|
||||
@@ -365,6 +372,10 @@ async function submitEditNode() {
|
||||
}
|
||||
|
||||
async function deleteNode(node: Record<string, any>) {
|
||||
if (!canDeleteByCreator(node)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定要删除节点「${node.nodeName}」吗?`, '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
v-if="hasCluePerm('remove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="clueInfo.isConverted === 1"
|
||||
:disabled="clueInfo.isConverted === 1 || !canDeleteByCreator(clueInfo)"
|
||||
:title="canDeleteByCreator(clueInfo) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
@@ -89,7 +90,7 @@
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasCluePerm('follow') && canManageFollow(item)"
|
||||
v-if="hasCluePerm('follow') && canDeleteByCreator(item)"
|
||||
class="follow-actions"
|
||||
>
|
||||
<el-button link type="primary" size="small" @click="handleEditFollow(item)">
|
||||
@@ -148,6 +149,7 @@
|
||||
import type { ClueVO, ClueFollowVO, ClueFollowQuery } from '@/api/crm/clue/types'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useCluePerm } from '@/hooks/business/useCluePerm'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import ClueForm from './form.vue'
|
||||
import FollowForm from './followForm.vue'
|
||||
@@ -336,11 +338,6 @@
|
||||
clueFormRef.value?.open(clueId.value)
|
||||
}
|
||||
|
||||
const canManageFollow = (item: ClueFollowVO) => {
|
||||
if (!item.createBy) return true
|
||||
return String(item.createBy) === String(currentUserId.value)
|
||||
}
|
||||
|
||||
const handleAddFollow = () => {
|
||||
followFormRef.value?.open(clueId.value)
|
||||
}
|
||||
|
||||
@@ -406,7 +406,8 @@
|
||||
<el-button
|
||||
v-if="hasCluePerm('remove')"
|
||||
class="btn-op-item danger"
|
||||
:disabled="row.isConverted === 1"
|
||||
:disabled="row.isConverted === 1 || !canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
@@ -549,6 +550,7 @@
|
||||
import type { UploadFile } from 'element-plus'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useCluePerm } from '@/hooks/business/useCluePerm'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
listClue,
|
||||
listMyClue,
|
||||
@@ -823,6 +825,16 @@
|
||||
ElMessage.warning('请至少选择一条数据进行删除')
|
||||
return
|
||||
}
|
||||
const targets = row ? [row] : selectedRows.value
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('是否确认删除选中的线索?已转化的线索不允许删除。', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('changeRemove')" :span="1.5">
|
||||
<el-button type="danger" icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
<el-button type="danger" icon="Delete" :disabled="multiple || !allRowsDeletableByCreator(changeList.filter((item) => ids.includes(item.id)))" :title="allRowsDeletableByCreator(changeList.filter((item) => ids.includes(item.id))) ? '' : CREATOR_DELETE_TIP" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('changeExport')" :span="1.5">
|
||||
<el-button type="success" icon="Download" @click="handleExport">导出</el-button>
|
||||
@@ -135,7 +135,7 @@
|
||||
icon="RefreshLeft"
|
||||
>撤回</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="hasContractPerm('changeRemove') && scope.row.approvalStatus === 'pending'"
|
||||
v-if="hasContractPerm('changeRemove') && scope.row.approvalStatus === 'pending' && canDeleteByCreator(scope.row)"
|
||||
command="delete"
|
||||
icon="Delete"
|
||||
divided
|
||||
@@ -238,6 +238,7 @@ import { useDictStore } from '@/store/modules/dict'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
const router = useRouter()
|
||||
const dictStore = useDictStore()
|
||||
@@ -455,6 +456,16 @@ const submitForm = () => {
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: ChangeVO) => {
|
||||
const targets = row ? [row] : changeList.value.filter((item) => ids.value.includes(item.id))
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const idList = row?.id ? [row.id] : ids.value;
|
||||
await $modal.confirm('是否确认删除选中的变更记录?')
|
||||
await delChange(idList)
|
||||
|
||||
@@ -37,7 +37,11 @@
|
||||
>
|
||||
<el-icon><Edit /></el-icon>修改
|
||||
</el-button>
|
||||
<el-button v-if="hasContractPerm('remove')" class="btn-ghost" @click="handleDetailDelete">
|
||||
<el-button
|
||||
v-if="hasContractPerm('remove') && canDeleteByCreator(detailContract)"
|
||||
class="btn-ghost"
|
||||
@click="handleDetailDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
@@ -161,19 +165,37 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" prop="quantity" width="110" align="right" />
|
||||
<el-table-column label="含税单价(元)" prop="price" width="130" align="right">
|
||||
<el-table-column
|
||||
v-if="!hideProductPrice"
|
||||
label="含税单价(元)"
|
||||
prop="price"
|
||||
width="130"
|
||||
align="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-else class="money-display">¥{{ parseFloat(scope.row.price || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="含税总价(元)" prop="subtotal" width="130" align="right">
|
||||
<el-table-column
|
||||
v-if="!hideProductPrice"
|
||||
label="含税总价(元)"
|
||||
prop="subtotal"
|
||||
width="130"
|
||||
align="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-else class="money-display">¥{{ parseFloat(scope.row.subtotal || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税费(元)" prop="taxAmount" width="120" align="right">
|
||||
<el-table-column
|
||||
v-if="!hideProductPrice"
|
||||
label="税费(元)"
|
||||
prop="taxAmount"
|
||||
width="120"
|
||||
align="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-else class="money-display">¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
|
||||
@@ -198,7 +220,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="detailContract.contractType === 'framework'">
|
||||
<template v-if="detailContract.contractType === 'framework' && !hideProductPrice">
|
||||
<el-divider content-position="left">框架合同调价明细</el-divider>
|
||||
<p class="text-muted" style="font-size: 13px; margin-bottom: 12px">按生效区间记录含税单价及调价函;后续结算可据此核对。</p>
|
||||
<div
|
||||
@@ -247,6 +269,7 @@
|
||||
</template>
|
||||
|
||||
<!-- 合计金额 -->
|
||||
<template v-if="!hideProductPrice">
|
||||
<el-divider />
|
||||
<div class="total-section">
|
||||
<el-row :gutter="20">
|
||||
@@ -320,6 +343,7 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</div>
|
||||
<el-empty v-else description="加载中..." />
|
||||
</el-tab-pane>
|
||||
@@ -573,7 +597,7 @@
|
||||
<el-descriptions-item label="完成说明" :span="2">
|
||||
<div class="remark-content">{{ detailContract?.remark || '-' }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行情况汇总" :span="2">
|
||||
<el-descriptions-item v-if="!hideProductPrice" label="执行情况汇总" :span="2">
|
||||
<el-row :gutter="20" class="mt-2">
|
||||
<el-col :span="8">
|
||||
<el-statistic title="合同金额(元)" :value="detailContract?.amount || 0" :precision="2" class="money-statistic">
|
||||
@@ -794,6 +818,7 @@ import DictTag from '@/components/DictTag/index.vue';
|
||||
import { $modal } from '@/utils/modal';
|
||||
import { parseTime } from '@/utils/format';
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm';
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete';
|
||||
import ApprovalDialog from './ApprovalDialog.vue';
|
||||
import ExecuteDialog from './ExecuteDialog.vue';
|
||||
import InvalidDialog from './InvalidDialog.vue';
|
||||
@@ -824,6 +849,15 @@ import {
|
||||
const dictStore = useDictStore();
|
||||
const router = useRouter();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
hideProductPrice?: boolean
|
||||
}>(),
|
||||
{
|
||||
hideProductPrice: false
|
||||
}
|
||||
)
|
||||
|
||||
const visible = ref(false);
|
||||
const detailLoading = ref(false);
|
||||
const detailActiveTab = ref('contract');
|
||||
@@ -1330,7 +1364,9 @@ const detailBasicViewFields = computed<ViewField[]>(() => {
|
||||
long: false
|
||||
},
|
||||
{ label: '负责人', value: c.ownerUserName },
|
||||
{ label: '合同金额(元)', value: `¥${formatDetailAmount(c.amount)}`, money: true },
|
||||
...(props.hideProductPrice
|
||||
? []
|
||||
: [{ label: '合同金额(元)', value: `¥${formatDetailAmount(c.amount)}`, money: true }]),
|
||||
{ label: '交易主体', value: c.supplierName },
|
||||
detailTagField('结算方式', 'crm_settlement_method', c.settlementMethod)
|
||||
];
|
||||
|
||||
@@ -40,15 +40,6 @@
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联商机" prop="opportunityId">
|
||||
<el-input v-model="form.opportunityName" placeholder="请选择商机(可选)" readonly clearable>
|
||||
<template #append>
|
||||
<el-button text @click="handleSelectOpportunity">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联报价单" prop="quoteId">
|
||||
<el-input v-model="form.quoteNo" placeholder="请选择报价单(可选)" readonly clearable>
|
||||
<template #append>
|
||||
@@ -501,14 +492,6 @@
|
||||
@confirm="handleContactConfirm"
|
||||
/>
|
||||
|
||||
<!-- 商机选择器 -->
|
||||
<OpportunitySelector
|
||||
:visible="opportunitySelectorVisible"
|
||||
@update:visible="opportunitySelectorVisible = $event"
|
||||
:customerId="form.customerId"
|
||||
@confirm="handleOpportunityConfirm"
|
||||
/>
|
||||
|
||||
<!-- 报价单选择器 -->
|
||||
<QuoteSelector
|
||||
:visible="quoteSelectorVisible"
|
||||
@@ -563,7 +546,6 @@ import { getQuote } from '@/api/crm/quote'
|
||||
import ProductSelector from './ProductSelector.vue'
|
||||
import CustomerSelector from '@/components/CustomerSelector.vue'
|
||||
import ContactSelector from '@/components/ContactSelector.vue'
|
||||
import OpportunitySelector from '@/components/OpportunitySelector.vue'
|
||||
import QuoteSelector from '@/components/QuoteSelector.vue'
|
||||
import ApprovalDialog from './ApprovalDialog.vue'
|
||||
import { getContactByCustomerId } from '@/api/crm/customer'
|
||||
|
||||
@@ -28,7 +28,12 @@
|
||||
@click="handleEdit"
|
||||
v-if="hasContractPerm('edit') && contract?.status === 'draft'"
|
||||
>修改</el-button>
|
||||
<el-button type="danger" icon="Delete" @click="handleDelete" v-if="hasContractPerm('remove')">删除</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('remove') && canDeleteByCreator(contract)"
|
||||
type="danger"
|
||||
icon="Delete"
|
||||
@click="handleDelete"
|
||||
>删除</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
icon="Promotion"
|
||||
@@ -1021,6 +1026,7 @@ import {
|
||||
downloadCrmAttachment
|
||||
} from '@/utils/crm-attachment';
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm';
|
||||
import { canDeleteByCreator } from '@/utils/creator-delete';
|
||||
import html2canvas from 'html2canvas';
|
||||
import { jsPDF } from 'jspdf';
|
||||
|
||||
@@ -1225,47 +1231,17 @@ const getOperLogList = async (reload: boolean = false) => {
|
||||
if (!contract.value?.id) return;
|
||||
logLoading.value = true;
|
||||
try {
|
||||
// 如果需要重新加载或者还没有加载过完整数据,则从服务器获取
|
||||
if (reload || operLogListFull.value.length === 0) {
|
||||
// 获取所有操作日志,不限制模块
|
||||
const query = {
|
||||
pageNum: 1,
|
||||
pageSize: 100 // 获取更多记录用于筛选
|
||||
};
|
||||
const res = await listOperLog(query);
|
||||
const payload = res as { rows?: OperLogVO[]; list?: OperLogVO[] } | OperLogVO[];
|
||||
const rows = Array.isArray(payload) ? payload : payload?.rows || payload?.list || [];
|
||||
|
||||
// 过滤出与当前合同相关的日志
|
||||
const contractIdStr = String(contract.value.id);
|
||||
operLogListFull.value = rows.filter((log: OperLogVO) => {
|
||||
// 检查URL是否包含合同ID
|
||||
const urlMatch = log.operUrl && log.operUrl.includes(`/crm/contract`) && log.operUrl.includes(`/${contractIdStr}`);
|
||||
|
||||
// 检查参数中是否包含合同ID(支持多种格式)
|
||||
const paramMatch =
|
||||
log.operParam &&
|
||||
(log.operParam.includes(`"id":${contractIdStr}`) ||
|
||||
log.operParam.includes(`"id":"${contractIdStr}"`) ||
|
||||
log.operParam.includes(`"contractId":${contractIdStr}`) ||
|
||||
log.operParam.includes(`"contractId":"${contractIdStr}"`) ||
|
||||
log.operParam.includes(`[${contractIdStr}]`) ||
|
||||
log.operParam.includes(`["${contractIdStr}"]`));
|
||||
|
||||
return urlMatch || paramMatch;
|
||||
});
|
||||
|
||||
// 按时间倒序排序
|
||||
operLogListFull.value.sort((a, b) => {
|
||||
return new Date(b.operTime).getTime() - new Date(a.operTime).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
// 从完整列表中进行分页
|
||||
const start = (logQuery.value.pageNum - 1) * logQuery.value.pageSize;
|
||||
const end = start + logQuery.value.pageSize;
|
||||
logTotal.value = operLogListFull.value.length;
|
||||
operLogList.value = operLogListFull.value.slice(start, end);
|
||||
const query = {
|
||||
pageNum: logQuery.value.pageNum,
|
||||
pageSize: logQuery.value.pageSize,
|
||||
businessId: String(contract.value.id),
|
||||
businessTitle: 'CRM合同管理'
|
||||
};
|
||||
const res = await listOperLog(query);
|
||||
const payload = res as { rows?: OperLogVO[]; list?: OperLogVO[]; total?: number };
|
||||
operLogListFull.value = payload?.rows || payload?.list || [];
|
||||
logTotal.value = payload?.total ?? operLogListFull.value.length;
|
||||
operLogList.value = operLogListFull.value;
|
||||
} catch (error) {
|
||||
console.error('获取操作日志失败:', error);
|
||||
operLogList.value = [];
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 第一行:客户名称、关联商机、关联报价单、联系人 -->
|
||||
<!-- 第一行:客户名称、关联报价单、联系人 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="客户名称" prop="customerId" required>
|
||||
@@ -51,15 +51,6 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="关联商机" prop="opportunityId">
|
||||
<el-input v-model="form.opportunityName" placeholder="请选择商机(可选)" readonly clearable>
|
||||
<template #append>
|
||||
<el-button text icon="Search" @click="handleSelectOpportunity" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="关联报价单" prop="quoteId">
|
||||
<el-input v-model="form.quoteNo" placeholder="请选择报价单(可选)" readonly clearable>
|
||||
@@ -560,14 +551,6 @@
|
||||
@confirm="handleContactConfirm"
|
||||
/>
|
||||
|
||||
<!-- 商机选择器 -->
|
||||
<OpportunitySelector
|
||||
:visible="opportunitySelectorVisible"
|
||||
@update:visible="opportunitySelectorVisible = $event"
|
||||
:customerId="form.customerId"
|
||||
@confirm="handleOpportunityConfirm"
|
||||
/>
|
||||
|
||||
<!-- 报价单选择器 -->
|
||||
<QuoteSelector
|
||||
:visible="quoteSelectorVisible"
|
||||
@@ -612,7 +595,6 @@ import { getQuote } from '@/api/crm/quote';
|
||||
import ProductSelector from './components/ProductSelector.vue';
|
||||
import CustomerSelector from '@/components/CustomerSelector.vue';
|
||||
import ContactSelector from '@/components/ContactSelector.vue';
|
||||
import OpportunitySelector from '@/components/OpportunitySelector.vue';
|
||||
import QuoteSelector from '@/components/QuoteSelector.vue';
|
||||
import UserSelector from '@/components/UserSelector.vue';
|
||||
import ApprovalDialog from './components/ApprovalDialog.vue';
|
||||
|
||||
@@ -571,6 +571,7 @@ import { $modal } from '@/utils/modal'
|
||||
import { parseTime } from '@/utils/format'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm'
|
||||
import { filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import ApprovalDialog from './components/ApprovalDialog.vue'
|
||||
import ExecuteDialog from './components/ExecuteDialog.vue'
|
||||
import InvalidDialog from './components/InvalidDialog.vue'
|
||||
@@ -1175,6 +1176,15 @@ const handleDelete = async (row?: ContractVO) => {
|
||||
$modal.msgWarning('只能删除草稿状态的合同');
|
||||
return;
|
||||
}
|
||||
const deletable = filterDeletableByCreator(selectedContracts)
|
||||
if (!deletable.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedContracts.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await $modal.confirm('是否确认删除选中的合同?只能删除草稿状态的合同');
|
||||
await delContract(idList);
|
||||
$modal.msgSuccess('删除成功');
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('templateRemove')" :span="1.5">
|
||||
<el-button type="danger" icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
<el-button type="danger" icon="Delete" :disabled="multiple || !allRowsDeletableByCreator(templateList.filter((item) => ids.includes(item.id)))" :title="allRowsDeletableByCreator(templateList.filter((item) => ids.includes(item.id))) ? '' : CREATOR_DELETE_TIP" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('templateExport')" :span="1.5">
|
||||
<el-button type="success" icon="Download" @click="handleExport">导出</el-button>
|
||||
@@ -93,7 +93,7 @@
|
||||
<el-tooltip v-if="hasContractPerm('templateEdit')" content="编辑" placement="top">
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="hasContractPerm('templateRemove')" content="删除" placement="top">
|
||||
<el-tooltip v-if="hasContractPerm('templateRemove') && canDeleteByCreator(scope.row)" content="删除" placement="top">
|
||||
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
@@ -203,6 +203,7 @@ import { useDictStore } from '@/store/modules/dict'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const { loadPerms, hasContractPerm } = useContractPerm()
|
||||
@@ -365,6 +366,16 @@ const submitForm = () => {
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: TemplateVO) => {
|
||||
const targets = row ? [row] : templateList.value.filter((item) => ids.value.includes(item.id))
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const idList = row?.id ? [row.id] : ids.value;
|
||||
await $modal.confirm('是否确认删除选中的合同模板?')
|
||||
await delTemplate(idList)
|
||||
|
||||
@@ -36,7 +36,13 @@
|
||||
<el-button class="btn-main" type="primary" @click="handleEdit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" type="danger" @click="handleDelete">
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(customerInfo)"
|
||||
:title="canDeleteByCreator(customerInfo) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -73,14 +79,11 @@
|
||||
}}</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="
|
||||
item.ownerUserName === userStore.info.realName ||
|
||||
item.createByName === userStore.info.realName ||
|
||||
userStore.info.roles?.includes('admin')
|
||||
"
|
||||
v-if="canDeleteByCreator(item)"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:title="canDeleteByCreator(item) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDeleteFollow(item)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
@@ -199,7 +202,13 @@
|
||||
<el-button link type="primary" @click="handleEditContact(row)">
|
||||
<el-icon><Edit /></el-icon>修改
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDeleteContact(row)">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDeleteContact(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -487,6 +496,7 @@ import { list as listOperLog } from '@/api/monitor/operlog'
|
||||
import type { OperLogVO } from '@/api/monitor/operlog/types'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import CustomerForm from './CustomerFormDialog.vue'
|
||||
import FollowForm from './FollowFormDialog.vue'
|
||||
import ContactForm from './components/ContactFormDialog.vue'
|
||||
@@ -751,39 +761,16 @@ async function getOperLogList(reload = false) {
|
||||
if (!customerId.value) return
|
||||
logLoading.value = true
|
||||
try {
|
||||
if (reload || !operLogListFull.value.length) {
|
||||
const res = await listOperLog({ pageNum: 1, pageSize: 100 })
|
||||
const payload = res as { rows?: OperLogVO[]; list?: OperLogVO[] } | OperLogVO[]
|
||||
const rows = Array.isArray(payload)
|
||||
? payload
|
||||
: payload?.rows || payload?.list || []
|
||||
const customerIdStr = String(customerId.value)
|
||||
const matchCustomerId = (text?: string) => {
|
||||
if (!text) return false
|
||||
return (
|
||||
text.includes(`"id":${customerIdStr}`) ||
|
||||
text.includes(`"id":"${customerIdStr}"`) ||
|
||||
text.includes(`"customerId":${customerIdStr}`) ||
|
||||
text.includes(`"customerId":"${customerIdStr}"`) ||
|
||||
text.includes(`[${customerIdStr}]`) ||
|
||||
text.includes(`["${customerIdStr}"]`) ||
|
||||
text.includes(`/${customerIdStr}`)
|
||||
)
|
||||
}
|
||||
operLogListFull.value = rows.filter((log: OperLogVO) => {
|
||||
const urlMatch =
|
||||
log.operUrl?.includes('/crm/customer') && matchCustomerId(log.operUrl)
|
||||
const paramMatch = matchCustomerId(log.operParam)
|
||||
const resultMatch = matchCustomerId(log.jsonResult)
|
||||
return urlMatch || paramMatch || resultMatch
|
||||
})
|
||||
operLogListFull.value.sort(
|
||||
(a, b) => new Date(b.operTime || 0).getTime() - new Date(a.operTime || 0).getTime()
|
||||
)
|
||||
}
|
||||
const start = (logQuery.value.pageNum - 1) * logQuery.value.pageSize
|
||||
logTotal.value = operLogListFull.value.length
|
||||
operLogList.value = operLogListFull.value.slice(start, start + logQuery.value.pageSize)
|
||||
const res = await listOperLog({
|
||||
pageNum: logQuery.value.pageNum,
|
||||
pageSize: logQuery.value.pageSize,
|
||||
businessId: String(customerId.value),
|
||||
businessTitle: 'CRM客户管理'
|
||||
})
|
||||
const payload = res as { rows?: OperLogVO[]; list?: OperLogVO[]; total?: number }
|
||||
operLogListFull.value = payload?.rows || payload?.list || []
|
||||
logTotal.value = payload?.total ?? operLogListFull.value.length
|
||||
operLogList.value = operLogListFull.value
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
operLogList.value = []
|
||||
|
||||
@@ -75,7 +75,13 @@
|
||||
<el-button v-if="row.isPrimary !== 1" link type="warning" @click="handleSetPrimary(row)">
|
||||
<el-icon><Star /></el-icon>设为主要
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -90,6 +96,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
fetchContactsByCustomerId,
|
||||
fetchDeleteContact,
|
||||
|
||||
@@ -99,7 +99,13 @@
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -162,6 +168,7 @@ import { ref, watch } from 'vue'
|
||||
import { Plus, Edit, Delete, Star, Close, Check } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/http'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
interface InvoiceInfoVO {
|
||||
id: string | number
|
||||
|
||||
@@ -607,7 +607,12 @@
|
||||
<el-button class="btn-op-item" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item btn-op-danger" @click="handleDelete(row)">
|
||||
<el-button
|
||||
class="btn-op-item btn-op-danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -703,6 +708,7 @@ import {
|
||||
Edit,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
fetchCustomerList,
|
||||
fetchCustomerDetail,
|
||||
@@ -983,6 +989,16 @@ async function handleDelete(row?: any) {
|
||||
ElMessage.warning('请至少选择一条数据进行删除')
|
||||
return
|
||||
}
|
||||
const targets = row ? [row] : selectedRows.value
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('是否确认删除选中的客户数据?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
<el-button
|
||||
class="btn-op-item danger"
|
||||
v-auth="'delete'"
|
||||
v-if="canDeleteByCreator(row)"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -224,6 +225,7 @@ import {
|
||||
fetchExecutePoolRules,
|
||||
} from '@/api/crm/pool'
|
||||
import RuleDialog from './components/RuleDialog.vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
v-if="hasOpportunityPerm('remove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="opportunityInfo.status === 'won'"
|
||||
:disabled="opportunityInfo.status === 'won' || !canDeleteByCreator(opportunityInfo)"
|
||||
:title="canDeleteByCreator(opportunityInfo) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
@@ -79,7 +80,7 @@
|
||||
</el-tag>
|
||||
<span class="follow-creator">{{ item.followByName || item.createByName || '—' }}</span>
|
||||
</div>
|
||||
<div v-if="String(item.followBy) === String(currentUserId)" class="follow-actions">
|
||||
<div v-if="canDeleteByCreator(item)" class="follow-actions">
|
||||
<el-button link type="primary" size="small" @click="handleEditFollow(item)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
@@ -190,6 +191,7 @@ import AttachmentUploadField from '@/components/business/attachment-upload-field
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useOpportunityPerm } from '@/hooks/business/useOpportunityPerm'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
getOpportunity,
|
||||
listFollowByOpportunity,
|
||||
|
||||
@@ -459,6 +459,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useOpportunityPerm } from '@/hooks/business/useOpportunityPerm'
|
||||
import { filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import { listOpportunity, listMyOpportunity, delOpportunity, assignOpportunity, exportOpportunity } from '@/api/crm/opportunity'
|
||||
import type { OpportunityQuery, OpportunityVO } from '@/api/crm/opportunity/types'
|
||||
@@ -777,6 +778,16 @@ const handleDelete = async (row?: OpportunityVO) => {
|
||||
ElMessage.warning('已赢单的商机不允许删除');
|
||||
return;
|
||||
}
|
||||
const targets = row ? [row] : opportunityList.value.filter((item) => ids.value.includes(item.id))
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const idList = row?.id ? [row.id] : ids.value;
|
||||
await ElMessageBox.confirm('是否确认删除选中的商机数据?删除后将无法恢复!', '提示', {
|
||||
type: 'warning'
|
||||
|
||||
@@ -75,7 +75,8 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="!selectedId"
|
||||
:disabled="!selectedId || !canDeleteSelectedCategory"
|
||||
:title="canDeleteSelectedCategory ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -219,7 +220,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { ref, reactive, onMounted, nextTick, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import {
|
||||
@@ -232,6 +233,7 @@ import {
|
||||
RefreshLeft,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
fetchCategoryTree,
|
||||
fetchCreateCategory,
|
||||
@@ -249,6 +251,12 @@ const refreshTable = ref(true)
|
||||
const selectedId = ref<string>()
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
const canDeleteSelectedCategory = computed(() => {
|
||||
if (!selectedId.value) return false
|
||||
const node = findNodeById(categoryList.value, selectedId.value)
|
||||
return canDeleteByCreator(node)
|
||||
})
|
||||
|
||||
// ── 弹窗数据 ──
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
@@ -377,6 +385,11 @@ async function handleDelete() {
|
||||
ElMessage.warning('请选择一条数据进行删除')
|
||||
return
|
||||
}
|
||||
const node = findNodeById(categoryList.value, selectedId.value)
|
||||
if (!canDeleteByCreator(node)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('是否确认删除该产品分类?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
<el-dropdown-item command="view">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -490,6 +490,7 @@ import SystemViewDialog, {
|
||||
import { crmDictField, crmField, crmLongField, crmTagField } from '@/utils/crm/view-fields'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import {
|
||||
fetchCategoryTree,
|
||||
@@ -735,6 +736,16 @@ async function handleDelete(row?: any) {
|
||||
ElMessage.warning('请至少选择一条数据进行删除')
|
||||
return
|
||||
}
|
||||
const targets = row ? [row] : selectedRows.value
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('是否确认删除选中的产品?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -53,9 +53,10 @@
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="quoteInfo.status !== 'invalid'"
|
||||
v-if="quoteInfo.status !== 'invalid' && canDeleteByCreator(quoteInfo)"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:title="canDeleteByCreator(quoteInfo) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
@@ -312,6 +313,7 @@ const CustomerDetailDialog = defineAsyncComponent(
|
||||
)
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useQuotePerm } from '@/hooks/business/useQuotePerm'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { parseTime } from '@/utils/format'
|
||||
import {
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
<el-button v-if="quote?.status === 'draft'" link type="primary" @click="handleSubmit">
|
||||
<el-icon><CircleCheck /></el-icon>提交审批
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete">
|
||||
<el-button
|
||||
v-if="quote?.status === 'draft' && canDeleteByCreator(quote)"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -384,6 +389,7 @@ const dictStore = useDictStore()
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import ListPagination from '@/components/ListPagination/index.vue'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { canDeleteByCreator } from '@/utils/creator-delete'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Back, CircleCheck, Close, Delete, Document, Download, Edit, Printer } from '@element-plus/icons-vue'
|
||||
|
||||
@@ -816,6 +816,7 @@ import html2canvas from 'html2canvas';
|
||||
import jsPDF from 'jspdf';
|
||||
|
||||
import { useQuotePerm } from '@/hooks/business/useQuotePerm'
|
||||
import { filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import QuoteFormDialog from './components/QuoteFormDialog.vue'
|
||||
import QuoteDetailDialog from './components/QuoteDetailDialog.vue'
|
||||
|
||||
@@ -1425,6 +1426,16 @@ const handleDelete = async (row?: QuoteVO) => {
|
||||
$modal.msgWarning('只能删除草稿状态的报价单');
|
||||
return;
|
||||
}
|
||||
const targets = row ? [row] : quoteList.value.filter((item) => ids.value.includes(item.id))
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const idList = row?.id ? [row.id] : ids.value;
|
||||
await $modal.confirm('是否确认删除选中的报价单?只能删除草稿状态的报价单');
|
||||
await delQuote(idList);
|
||||
|
||||
@@ -371,8 +371,8 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item danger"
|
||||
:disabled="!canDeleteRecord(row)"
|
||||
:title="canDeleteRecord(row) ? '' : '仅支持删除当天及之后日期的记录'"
|
||||
:disabled="!canDeleteRecord(row) || !canDeleteByCreator(row)"
|
||||
:title="!canDeleteRecord(row) ? '仅支持删除当天及之后日期的记录' : canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -514,6 +514,7 @@
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
|
||||
import DbInventoryRelatedViewDialog from '@/components/business/db/DbInventoryRelatedViewDialog.vue'
|
||||
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
|
||||
@@ -884,6 +885,10 @@
|
||||
$modal.msgWarning('仅支持删除当天及之后日期的记录,历史数据不可删除')
|
||||
return
|
||||
}
|
||||
if (!canDeleteByCreator(row)) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await $modal.confirm('确定要删除这条记录吗?')
|
||||
await inventoryApi.delete(row.id)
|
||||
|
||||
@@ -287,7 +287,12 @@
|
||||
<el-icon><DocumentAdd /></el-icon>
|
||||
生成出入库单
|
||||
</el-button>
|
||||
<el-button class="btn-op-item danger" @click="handleDelete(row.id)">
|
||||
<el-button
|
||||
class="btn-op-item danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
@@ -459,6 +464,7 @@
|
||||
import DbPlanRelatedViewDialog from '@/components/business/db/DbPlanRelatedViewDialog.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
planApi,
|
||||
type Plan,
|
||||
@@ -769,10 +775,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const handleDelete = async (row: Plan) => {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await $modal.confirm('确定要删除这个计划吗?')
|
||||
await planApi.delete(id)
|
||||
await planApi.delete(row.id)
|
||||
$modal.msgSuccess('删除成功')
|
||||
loadData()
|
||||
} catch {
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
<el-option label="已终止" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核状态">
|
||||
<el-select v-model="searchForm.auditStatus" placeholder="全部" clearable>
|
||||
<el-option label="待审核" value="pending" />
|
||||
<el-option label="已通过" value="approved" />
|
||||
<el-option label="已驳回" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
@@ -73,6 +80,18 @@
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === '1' ? '合作中' : '已终止' }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="getPlCarrierAuditStatusTagType(row.auditStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ getPlCarrierAuditStatusLabel(row.auditStatus) }}
|
||||
</el-tag>
|
||||
<el-tag v-if="row.transportAgreementExpired" type="danger" size="small">
|
||||
协议已过期
|
||||
</el-tag>
|
||||
<el-tag v-else-if="row.qualificationComplete === false" type="warning" size="small">
|
||||
资质不全
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +119,16 @@
|
||||
<el-button link type="primary" @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click.stop="handleDelete(row)">
|
||||
<el-button
|
||||
v-if="canApproveCarrier(row)"
|
||||
link
|
||||
type="primary"
|
||||
:loading="auditLoadingMap[row.id]"
|
||||
@click.stop="handleApprove(row)"
|
||||
>
|
||||
<el-icon><Select /></el-icon>审核通过
|
||||
</el-button>
|
||||
<el-button link type="danger" :disabled="!canDeleteByCreator(row)" :title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -113,7 +141,7 @@
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="520px"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout"
|
||||
@close="handleDialogClose"
|
||||
@@ -145,6 +173,52 @@
|
||||
maxlength="512"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">资质附件</el-divider>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="营业执照" prop="businessLicenseUrls">
|
||||
<AttachmentUploadField
|
||||
ref="businessLicenseUploadRef"
|
||||
v-model="formData.businessLicenseUrls"
|
||||
:limit="5"
|
||||
accept="image/*,.pdf"
|
||||
folder="pl/carrier/license"
|
||||
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
|
||||
@update:model-value="handleCertFieldChange('businessLicenseUrls')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="道路经营许可证" prop="roadTransportLicenseUrls">
|
||||
<AttachmentUploadField
|
||||
ref="roadTransportLicenseUploadRef"
|
||||
v-model="formData.roadTransportLicenseUrls"
|
||||
:limit="5"
|
||||
accept="image/*,.pdf"
|
||||
folder="pl/carrier/permit"
|
||||
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
|
||||
@update:model-value="handleCertFieldChange('roadTransportLicenseUrls')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运输协议" prop="transportAgreementUrls" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="transportAgreementUploadRef"
|
||||
v-model="formData.transportAgreementUrls"
|
||||
:limit="5"
|
||||
accept="image/*,.pdf"
|
||||
folder="pl/carrier/agreement"
|
||||
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
|
||||
@update:model-value="handleCertFieldChange('transportAgreementUrls')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="协议有效期" prop="transportAgreementExpireDate">
|
||||
<el-date-picker
|
||||
v-model="formData.transportAgreementExpireDate"
|
||||
type="date"
|
||||
placeholder="请选择协议有效期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@@ -163,25 +237,36 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Select, View } from '@element-plus/icons-vue'
|
||||
import { carrierApi, type Carrier } from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlCarrierViewDialog from '@/components/business/pl-carrier-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
getPlCarrierAuditStatusLabel,
|
||||
getPlCarrierAuditStatusTagType
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const auditLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const selectedRows = ref<Carrier[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref()
|
||||
const businessLicenseUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const roadTransportLicenseUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const transportAgreementUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const isEdit = ref(false)
|
||||
const viewVisible = ref(false)
|
||||
const viewCarrierId = ref('')
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
status: ''
|
||||
status: '',
|
||||
auditStatus: ''
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
@@ -190,11 +275,70 @@ const formData = reactive({
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
status: '1',
|
||||
remark: ''
|
||||
remark: '',
|
||||
businessLicenseUrls: '',
|
||||
roadTransportLicenseUrls: '',
|
||||
transportAgreementUrls: '',
|
||||
transportAgreementExpireDate: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
carrierName: [{ required: true, message: '请输入承运商名称', trigger: 'blur' }]
|
||||
const createQualificationRules = {
|
||||
businessLicenseUrls: [
|
||||
{
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请上传营业执照'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
roadTransportLicenseUrls: [
|
||||
{
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请上传道路经营许可证'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
transportAgreementUrls: [
|
||||
{
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请上传运输协议'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
transportAgreementExpireDate: [
|
||||
{ required: true, message: '请选择协议有效期', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
const formRules = computed(() => ({
|
||||
carrierName: [{ required: true, message: '请输入承运商名称', trigger: 'blur' }],
|
||||
...(isEdit.value ? {} : createQualificationRules)
|
||||
}))
|
||||
|
||||
function handleCertFieldChange(
|
||||
field: 'businessLicenseUrls' | 'roadTransportLicenseUrls' | 'transportAgreementUrls'
|
||||
) {
|
||||
formRef.value?.validateField(field)
|
||||
}
|
||||
|
||||
function hasAttachmentUploading() {
|
||||
return [businessLicenseUploadRef, roadTransportLicenseUploadRef, transportAgreementUploadRef].some(
|
||||
item => item.value?.hasUploading?.()
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
@@ -202,7 +346,8 @@ async function fetchData() {
|
||||
try {
|
||||
const res = await carrierApi.list({
|
||||
q: searchForm.q || undefined,
|
||||
status: searchForm.status || undefined
|
||||
status: searchForm.status || undefined,
|
||||
auditStatus: searchForm.auditStatus || undefined
|
||||
})
|
||||
carrierList.value = (res.data ?? res) || []
|
||||
const idSet = new Set(carrierList.value.map(item => item.id))
|
||||
@@ -219,10 +364,35 @@ function handleSearch() {
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.status = ''
|
||||
searchForm.auditStatus = ''
|
||||
selectedRows.value = []
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function canApproveCarrier(row: Carrier) {
|
||||
return row.auditStatus !== 'approved' && row.qualificationComplete
|
||||
}
|
||||
|
||||
async function handleApprove(row: Carrier) {
|
||||
if (!canApproveCarrier(row)) {
|
||||
ElMessage.warning('承运商资质不完整,无法审核通过')
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(
|
||||
`确认审核通过承运商「${row.carrierName}」?通过后其车辆可用于运输安排。`,
|
||||
'审核确认',
|
||||
{ type: 'info', confirmButtonText: '审核通过', cancelButtonText: '取消' }
|
||||
)
|
||||
auditLoadingMap[row.id] = true
|
||||
try {
|
||||
await carrierApi.approve(row.id)
|
||||
ElMessage.success('审核通过')
|
||||
fetchData()
|
||||
} finally {
|
||||
auditLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
function isSelected(row: Carrier) {
|
||||
return selectedRows.value.some(item => item.id === row.id)
|
||||
}
|
||||
@@ -246,7 +416,11 @@ function handleAdd() {
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
status: '1',
|
||||
remark: ''
|
||||
remark: '',
|
||||
businessLicenseUrls: '',
|
||||
roadTransportLicenseUrls: '',
|
||||
transportAgreementUrls: '',
|
||||
transportAgreementExpireDate: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -260,7 +434,11 @@ function handleEdit(row: Carrier) {
|
||||
contactName: row.contactName,
|
||||
contactPhone: row.contactPhone,
|
||||
status: row.status,
|
||||
remark: row.remark
|
||||
remark: row.remark,
|
||||
businessLicenseUrls: row.businessLicenseUrls || '',
|
||||
roadTransportLicenseUrls: row.roadTransportLicenseUrls || '',
|
||||
transportAgreementUrls: row.transportAgreementUrls || '',
|
||||
transportAgreementExpireDate: row.transportAgreementExpireDate || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -273,13 +451,45 @@ function handleView(row: Carrier) {
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (hasAttachmentUploading()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
carrierName: formData.carrierName,
|
||||
contactName: formData.contactName,
|
||||
contactPhone: formData.contactPhone,
|
||||
status: formData.status,
|
||||
remark: formData.remark,
|
||||
...(isEdit.value
|
||||
? {
|
||||
...(formData.businessLicenseUrls
|
||||
? { businessLicenseUrls: formData.businessLicenseUrls }
|
||||
: {}),
|
||||
...(formData.roadTransportLicenseUrls
|
||||
? { roadTransportLicenseUrls: formData.roadTransportLicenseUrls }
|
||||
: {}),
|
||||
...(formData.transportAgreementUrls
|
||||
? { transportAgreementUrls: formData.transportAgreementUrls }
|
||||
: {}),
|
||||
...(formData.transportAgreementExpireDate
|
||||
? { transportAgreementExpireDate: formData.transportAgreementExpireDate }
|
||||
: {})
|
||||
}
|
||||
: {
|
||||
businessLicenseUrls: formData.businessLicenseUrls,
|
||||
roadTransportLicenseUrls: formData.roadTransportLicenseUrls,
|
||||
transportAgreementUrls: formData.transportAgreementUrls,
|
||||
transportAgreementExpireDate: formData.transportAgreementExpireDate
|
||||
})
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await carrierApi.update(formData.id, formData)
|
||||
await carrierApi.update(formData.id, payload)
|
||||
} else {
|
||||
await carrierApi.create(formData)
|
||||
await carrierApi.create(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
dialogVisible.value = false
|
||||
@@ -294,6 +504,10 @@ function handleDialogClose() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Carrier) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该承运商吗?', '提示', { type: 'warning' })
|
||||
await carrierApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
@@ -301,7 +515,7 @@ async function handleDelete(row: Carrier) {
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({ q: searchForm.q, status: searchForm.status }),
|
||||
() => ({ q: searchForm.q, status: searchForm.status, auditStatus: searchForm.auditStatus }),
|
||||
handleSearch
|
||||
)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">
|
||||
<el-button link type="danger" :disabled="!canDeleteByCreator(row)" :title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -177,6 +177,7 @@
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { driverApi, type Driver } from '@/api/pl'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
@@ -315,6 +316,10 @@ function handleDialogClose() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Driver) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该司机吗?', '提示', { type: 'warning' })
|
||||
await driverApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<el-icon><Promotion /></el-icon>提交审批
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.settlementStatus === 'pending'"
|
||||
v-if="row.settlementStatus === 'pending' && canDeleteByCreator(row)"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
@@ -165,6 +165,7 @@ import {
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import { carrierApi, freightApi, type Carrier, type FreightSettlement } from '@/api/pl'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import PlFreightApprovalDialog from '@/components/business/pl-freight-approval-dialog/index.vue'
|
||||
import PlFreightSettlementFormDialog from '@/components/business/pl-freight-settlement-form-dialog/index.vue'
|
||||
import PlFreightSettlementViewDialog from '@/components/business/pl-freight-settlement-view-dialog/index.vue'
|
||||
@@ -269,6 +270,10 @@ async function handleWithdraw(row: FreightSettlement) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: FreightSettlement) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该草稿结算单吗?关联运输单将释放。', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
@@ -61,11 +61,6 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stockAmount" label="库存金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatAmount(row.stockAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预警阈值" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.lowStockWarning || '-' }} ~ {{ row.highStockWarning || '-' }}
|
||||
@@ -108,7 +103,6 @@
|
||||
import { RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { inventoryApi, type InventoryRecord } from '@/api/pl'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
const loading = ref(false)
|
||||
const inventoryList = ref<InventoryRecord[]>([])
|
||||
@@ -123,11 +117,6 @@ const pagination = reactive({
|
||||
total: 0
|
||||
})
|
||||
|
||||
function formatAmount(amount: number) {
|
||||
if (!amount) return '¥0.00'
|
||||
return formatMoneyDisplay(amount)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
<el-button link type="primary" @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click.stop="handleDelete(row)">
|
||||
<el-button link type="danger" :disabled="!canDeleteByCreator(row)" :title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -219,6 +219,7 @@ import SystemViewDialog, {
|
||||
} from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
@@ -489,6 +490,10 @@ function handleDialogClose() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Personnel) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该人员吗?', '提示', { type: 'warning' })
|
||||
await personnelApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
@@ -154,11 +154,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalAmount" label="计划金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatAmount(row.totalAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="driverName" label="司机" min-width="100" align="center" />
|
||||
<el-table-column prop="followUserName" label="跟进人" min-width="100" align="center" />
|
||||
@@ -237,7 +232,6 @@ import {
|
||||
getPlPlanArchiveStatusLabel,
|
||||
getPlPlanArchiveStatusTagType
|
||||
} from '@/utils/pl-display'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
const loading = ref(false)
|
||||
const formDialogVisible = ref(false)
|
||||
@@ -299,11 +293,6 @@ function getFollowStatusLabel(status: string) {
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function formatAmount(amount: number) {
|
||||
if (!amount) return '¥0.00'
|
||||
return formatMoneyDisplay(amount)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
|
||||
@@ -132,11 +132,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalAmount" label="计划金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatAmount(row.totalAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="driverName" label="司机" min-width="100" align="center" />
|
||||
<el-table-column prop="followUserName" label="跟进人" min-width="100" align="center" />
|
||||
@@ -196,7 +191,6 @@ import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/ind
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
const loading = ref(false)
|
||||
const viewVisible = ref(false)
|
||||
@@ -254,11 +248,6 @@ function getFollowStatusLabel(status: string) {
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function formatAmount(amount: number) {
|
||||
if (!amount) return '¥0.00'
|
||||
return formatMoneyDisplay(amount)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
|
||||
@@ -103,7 +103,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="multiple"
|
||||
:disabled="multiple || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="deleteLoading"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
@@ -161,11 +162,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalAmount" label="计划金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatAmount(row.totalAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="driverName" label="司机" min-width="100" align="center" />
|
||||
<el-table-column prop="followUserName" label="跟进人" min-width="100" align="center" />
|
||||
@@ -201,7 +197,13 @@
|
||||
>
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -259,6 +261,7 @@ import {
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import PlPlanArrangeDialog from '@/components/business/pl-plan-arrange-dialog/index.vue'
|
||||
import PlPlanCertUploadDialog from '@/components/business/pl-plan-cert-upload-dialog/index.vue'
|
||||
import PlPlanFormDialog from '@/components/business/pl-plan-form-dialog/index.vue'
|
||||
@@ -266,8 +269,6 @@ import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/ind
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -336,11 +337,6 @@ function getFollowStatusLabel(status: string) {
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function formatAmount(amount: number) {
|
||||
if (!amount) return '¥0.00'
|
||||
return formatMoneyDisplay(amount)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -454,6 +450,10 @@ function handleArrange(row: Plan) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Plan) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除运输计划「${row.planNo}」吗?`, '提示', { type: 'warning' })
|
||||
deleteLoading.value = true
|
||||
try {
|
||||
@@ -467,11 +467,16 @@ async function handleDelete(row: Plan) {
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map(r => r.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请至少选择一项')
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map(r => r.id)
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条运输计划吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
@@ -24,6 +24,16 @@
|
||||
<el-option label="报废" value="scrapped" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="承运商">
|
||||
<el-select v-model="searchForm.carrierId" placeholder="全部" clearable filterable>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
:key="c.id"
|
||||
:label="c.carrierName"
|
||||
:value="c.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
@@ -84,6 +94,10 @@
|
||||
<dt>载重(吨)</dt>
|
||||
<dd>{{ row.loadCapacity || '—' }}</dd>
|
||||
</div>
|
||||
<div class="vehicle-card-kv-item">
|
||||
<dt>承运商</dt>
|
||||
<dd>{{ row.carrierName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="vehicle-card-kv-item">
|
||||
<dt>关联人员</dt>
|
||||
<dd>{{ row.personnelSummary || '—' }}</dd>
|
||||
@@ -101,7 +115,7 @@
|
||||
<el-button link type="primary" @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click.stop="handleDelete(row)">
|
||||
<el-button link type="danger" :disabled="!canDeleteByCreator(row)" :title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -135,6 +149,16 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="承运商" prop="carrierId">
|
||||
<el-select v-model="formData.carrierId" placeholder="请选择承运商(选填)" clearable filterable>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
:key="c.id"
|
||||
:label="c.carrierName"
|
||||
:value="c.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车辆状态" prop="vehicleStatus">
|
||||
<el-select v-model="formData.vehicleStatus" placeholder="请选择">
|
||||
<el-option label="可用" value="available" />
|
||||
@@ -216,9 +240,11 @@
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<PlCertFormList
|
||||
<PlVehicleCertPartTabs
|
||||
ref="certFormListRef"
|
||||
v-model="certList"
|
||||
:license-plate="formData.licensePlate"
|
||||
:trailer-no="formData.trailerNo"
|
||||
title="车辆证书"
|
||||
upload-folder="pl/vehicle-cert"
|
||||
:cert-name-options="certNameOptions"
|
||||
@@ -250,6 +276,8 @@
|
||||
<template #vehicleDetailTabs>
|
||||
<PlVehicleCertPersonnelTabs
|
||||
:cert-list="viewDetail?.certList || []"
|
||||
:license-plate="viewDetail?.licensePlate"
|
||||
:trailer-no="viewDetail?.trailerNo"
|
||||
:personnel-list="viewPersonnelList"
|
||||
/>
|
||||
</template>
|
||||
@@ -262,13 +290,15 @@ import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Van, View } from
|
||||
import {
|
||||
vehicleApi,
|
||||
personnelApi,
|
||||
carrierApi,
|
||||
type Vehicle,
|
||||
type VehicleDetail,
|
||||
type VehiclePersonnelRow,
|
||||
type CertRow,
|
||||
type Personnel
|
||||
type Personnel,
|
||||
type Carrier
|
||||
} from '@/api/pl'
|
||||
import PlCertFormList from '@/components/business/pl-cert-form-list/index.vue'
|
||||
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
|
||||
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import SystemViewDialog, {
|
||||
@@ -281,15 +311,22 @@ import {
|
||||
getPlVehicleStatusLabel,
|
||||
getPlVehicleStatusTagType
|
||||
} from '@/utils/pl-display'
|
||||
import {
|
||||
PL_VEHICLE_CERT_NAME_OPTIONS,
|
||||
splitVehicleCertsByPart,
|
||||
validateVehicleRequiredCerts
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
const VEHICLE_TYPE_SELF = 'self'
|
||||
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
type CertFormListRef = InstanceType<typeof PlVehicleCertPartTabs>
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const vehicleList = ref<Vehicle[]>([])
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const personnelOptions = ref<Personnel[]>([])
|
||||
const selectedRows = ref<Vehicle[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
@@ -305,18 +342,12 @@ const viewLoading = ref(false)
|
||||
const viewDetail = ref<VehicleDetail | null>(null)
|
||||
const viewPersonnelList = ref<VehiclePersonnelViewRow[]>([])
|
||||
|
||||
const certNameOptions = [
|
||||
'行驶证',
|
||||
'道路运输证',
|
||||
'驾驶证',
|
||||
'押运员证',
|
||||
'危化品从业资格证',
|
||||
'保险单'
|
||||
]
|
||||
const certNameOptions = PL_VEHICLE_CERT_NAME_OPTIONS
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
vehicleStatus: ''
|
||||
vehicleStatus: '',
|
||||
carrierId: ''
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
@@ -324,6 +355,8 @@ const formData = reactive({
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
loadCapacity: null as number | null,
|
||||
carrierId: '',
|
||||
carrierName: '',
|
||||
vehicleStatus: 'available',
|
||||
remark: ''
|
||||
})
|
||||
@@ -343,7 +376,11 @@ function getStatusLabel(status: string) {
|
||||
|
||||
const viewHeroMeta = computed(() => {
|
||||
if (!viewDetail.value) return ''
|
||||
return [getStatusLabel(viewDetail.value.vehicleStatus), '自有']
|
||||
return [
|
||||
getStatusLabel(viewDetail.value.vehicleStatus),
|
||||
'自有',
|
||||
viewDetail.value.carrierName || undefined
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
})
|
||||
@@ -359,6 +396,7 @@ const viewSections = computed<ViewSection[]>(() => {
|
||||
{ label: '车牌号', value: d.licensePlate },
|
||||
{ label: '挂车号', value: d.trailerNo || '—' },
|
||||
{ label: '车辆类型', value: '自有', tagType: 'success' },
|
||||
{ label: '承运商', value: d.carrierName || '—' },
|
||||
{ label: '载重(吨)', value: d.loadCapacity ?? '—' },
|
||||
{
|
||||
label: '车辆状态',
|
||||
@@ -506,24 +544,33 @@ function validateSubTables(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const cert of certList.value) {
|
||||
if (!cert.certName?.trim()) {
|
||||
ElMessage.warning('证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
const hasTrailer = Boolean(formData.trailerNo?.trim())
|
||||
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList.value)
|
||||
return validateVehicleRequiredCerts({
|
||||
headCerts,
|
||||
trailerCerts,
|
||||
hasTrailer,
|
||||
licensePlate: formData.licensePlate,
|
||||
trailerNo: formData.trailerNo,
|
||||
onError: message => ElMessage.warning(message)
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleApi.list({
|
||||
vehicleType: VEHICLE_TYPE_SELF,
|
||||
q: searchForm.q || undefined,
|
||||
vehicleStatus: searchForm.vehicleStatus || undefined
|
||||
})
|
||||
vehicleList.value = (res.data ?? res) || []
|
||||
const [vehicleRes, carrierRes] = await Promise.all([
|
||||
vehicleApi.list({
|
||||
vehicleType: VEHICLE_TYPE_SELF,
|
||||
q: searchForm.q || undefined,
|
||||
vehicleStatus: searchForm.vehicleStatus || undefined,
|
||||
carrierId: searchForm.carrierId || undefined
|
||||
}),
|
||||
carrierApi.list()
|
||||
])
|
||||
vehicleList.value = (vehicleRes.data ?? vehicleRes) || []
|
||||
carrierList.value = (carrierRes.data ?? carrierRes) || []
|
||||
const idSet = new Set(vehicleList.value.map(item => item.id))
|
||||
selectedRows.value = selectedRows.value.filter(item => idSet.has(item.id))
|
||||
} finally {
|
||||
@@ -538,6 +585,7 @@ function handleSearch() {
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.vehicleStatus = ''
|
||||
searchForm.carrierId = ''
|
||||
selectedRows.value = []
|
||||
fetchData()
|
||||
}
|
||||
@@ -564,6 +612,8 @@ function handleAdd() {
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
loadCapacity: null,
|
||||
carrierId: '',
|
||||
carrierName: '',
|
||||
vehicleStatus: 'available',
|
||||
remark: ''
|
||||
})
|
||||
@@ -608,6 +658,8 @@ async function handleEdit(row: Vehicle) {
|
||||
licensePlate: detail.licensePlate,
|
||||
trailerNo: detail.trailerNo || '',
|
||||
loadCapacity: detail.loadCapacity ?? null,
|
||||
carrierId: detail.carrierId || '',
|
||||
carrierName: detail.carrierName || '',
|
||||
vehicleStatus: detail.vehicleStatus,
|
||||
remark: detail.remark
|
||||
})
|
||||
@@ -620,6 +672,7 @@ async function handleEdit(row: Vehicle) {
|
||||
certList.value = (detail.certList || []).map(c => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate,
|
||||
@@ -641,12 +694,21 @@ async function handleSubmit() {
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.carrierId) {
|
||||
const carrier = carrierList.value.find(c => c.id === formData.carrierId)
|
||||
formData.carrierName = carrier?.carrierName || ''
|
||||
} else {
|
||||
formData.carrierName = ''
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
licensePlate: formData.licensePlate,
|
||||
trailerNo: formData.trailerNo || undefined,
|
||||
vehicleType: VEHICLE_TYPE_SELF,
|
||||
carrierId: formData.carrierId || undefined,
|
||||
carrierName: formData.carrierName || undefined,
|
||||
loadCapacity: formData.loadCapacity,
|
||||
vehicleStatus: formData.vehicleStatus,
|
||||
remark: formData.remark,
|
||||
@@ -661,6 +723,7 @@ async function handleSubmit() {
|
||||
})),
|
||||
certList: certList.value.map(c => ({
|
||||
certId: c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName.trim(),
|
||||
certNo: c.certNo || undefined,
|
||||
expireDate: c.expireDate || undefined,
|
||||
@@ -688,6 +751,10 @@ function handleDialogClose() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Vehicle) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该自有车辆吗?', '提示', { type: 'warning' })
|
||||
await vehicleApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
<!-- 车辆维保申请 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<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.q" placeholder="搜索申请单号或车牌" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="维保类型">
|
||||
<el-select
|
||||
v-model="searchForm.maintenanceType"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in PL_MAINTENANCE_TYPE_PRESETS"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||||
<el-option label="待处理" value="pending" />
|
||||
<el-option label="已处理" value="processed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @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"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="maintenanceType" label="维保类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlMaintenanceTypeTagType(row.maintenanceType)" size="small">
|
||||
{{ getPlMaintenanceTypeLabel(row.maintenanceType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyDate" label="申请日期" min-width="120" align="center" />
|
||||
<el-table-column prop="startDate" label="开始日期" min-width="120" align="center" />
|
||||
<el-table-column prop="expectedPickupDate" label="预计取车" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.expectedPickupDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="status" label="状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlMaintenanceApplyStatusTagType(row.status)" size="small">
|
||||
{{ getPlMaintenanceApplyStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<template v-if="row.status === 'pending'">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="warning" :loading="cancelLoadingMap[row.id]" @click="handleCancel(row)">
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button link type="danger" :loading="deleteLoadingMap[row.id]" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item v-if="formData.applyNo" label="申请单号">
|
||||
<el-input v-model="formData.applyNo" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请人" prop="applicantId">
|
||||
<el-select
|
||||
v-model="formData.applicantId"
|
||||
filterable
|
||||
placeholder="请选择申请人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.displayName"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
@change="handleLicensePlateChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="维保类型" prop="maintenanceType">
|
||||
<el-select
|
||||
v-model="formData.maintenanceType"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="请选择或输入维保类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in PL_MAINTENANCE_TYPE_PRESETS"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期" prop="applyDate">
|
||||
<el-date-picker v-model="formData.applyDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开始日期" prop="startDate">
|
||||
<el-date-picker v-model="formData.startDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预计取车日期">
|
||||
<el-date-picker
|
||||
v-model="formData.expectedPickupDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="可选"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="维保内容" prop="maintenanceContent" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="formData.maintenanceContent"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入维保内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="维修店地址" class="form-col-span-2">
|
||||
<el-input v-model="formData.shopAddress" placeholder="请输入维修店地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-maintenance/apply"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewDetail?.applyNo || '申请详情'"
|
||||
:sections="viewSections"
|
||||
:loading="viewLoading"
|
||||
width="720px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Check,
|
||||
CircleClose,
|
||||
Close,
|
||||
Delete,
|
||||
Edit,
|
||||
Plus,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleMaintenanceApplyApi,
|
||||
type Vehicle,
|
||||
type VehicleMaintenanceApply
|
||||
} from '@/api/pl'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
getPlMaintenanceApplyStatusLabel,
|
||||
getPlMaintenanceApplyStatusTagType,
|
||||
getPlMaintenanceTypeLabel,
|
||||
getPlMaintenanceTypeTagType,
|
||||
PL_MAINTENANCE_TYPE_PRESETS
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
})
|
||||
|
||||
const userOptions = ref<
|
||||
Array<{ userId: string; nickName: string; displayName: string }>
|
||||
>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const cancelLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleMaintenanceApply[]>([])
|
||||
const selectedRows = ref<VehicleMaintenanceApply[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
maintenanceType: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
applyNo: '',
|
||||
applicantId: '',
|
||||
licensePlate: '',
|
||||
maintenanceType: '',
|
||||
applyDate: '',
|
||||
startDate: '',
|
||||
expectedPickupDate: '',
|
||||
maintenanceContent: '',
|
||||
shopAddress: '',
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
applicantId: [{ required: true, message: '请选择申请人', trigger: 'change' }],
|
||||
licensePlate: [{ required: true, message: '请选择车牌号', trigger: 'change' }],
|
||||
maintenanceType: [{ required: true, message: '请选择维保类型', trigger: 'change' }],
|
||||
applyDate: [{ required: true, message: '请选择申请日期', trigger: 'change' }],
|
||||
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewLoading = ref(false)
|
||||
const viewDetail = ref<VehicleMaintenanceApply | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
return [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: d.applyNo },
|
||||
{ label: '车牌号', value: d.licensePlate },
|
||||
{
|
||||
label: '维保类型',
|
||||
value: getPlMaintenanceTypeLabel(d.maintenanceType),
|
||||
tagType: getPlMaintenanceTypeTagType(d.maintenanceType)
|
||||
},
|
||||
{ label: '申请日期', value: d.applyDate },
|
||||
{ label: '开始日期', value: d.startDate },
|
||||
{ label: '预计取车', value: d.expectedPickupDate || '—' },
|
||||
{ label: '申请人', value: d.applicantName || '—' },
|
||||
{
|
||||
label: '状态',
|
||||
value: getPlMaintenanceApplyStatusLabel(d.status),
|
||||
tagType: getPlMaintenanceApplyStatusTagType(d.status)
|
||||
},
|
||||
{ label: '维保内容', value: d.maintenanceContent || '—', span: 2 },
|
||||
{ label: '维修店地址', value: d.shopAddress || '—', span: 2 },
|
||||
{ label: '备注', value: d.remark || '—', span: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: unknown = (res as { data?: unknown })?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: Record<string, unknown>) => {
|
||||
const name = String(u.label || u.nickName || '')
|
||||
const account = String(u.userNo || '')
|
||||
return {
|
||||
userId: String(u.value ?? u.id ?? ''),
|
||||
nickName: name,
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list()
|
||||
vehicleOptions.value = (res.data ?? res) || []
|
||||
}
|
||||
|
||||
async function showMaintenanceReminders(licensePlate: string) {
|
||||
if (!licensePlate) return
|
||||
try {
|
||||
const res = await vehicleMaintenanceApplyApi.reminders(
|
||||
licensePlate,
|
||||
isEdit.value ? formData.id : undefined
|
||||
)
|
||||
const items = (res.data ?? res) || []
|
||||
if (!items.length) return
|
||||
const text = items
|
||||
.map((item, index) => {
|
||||
const lines = [
|
||||
`【${index + 1}】申请单号:${item.applyNo}`,
|
||||
`维保类型:${item.maintenanceType}`,
|
||||
`开始日期:${item.startDate}`,
|
||||
`预计取车:${item.expectedPickupDate || '—'}`,
|
||||
`维保内容:${item.maintenanceContent || '—'}`,
|
||||
`维修店地址:${item.shopAddress || '—'}`
|
||||
]
|
||||
return lines.join('\n')
|
||||
})
|
||||
.join('\n\n')
|
||||
await ElMessageBox.alert(text, '车辆维保提醒', { confirmButtonText: '知道了' })
|
||||
} catch {
|
||||
// ignore reminder fetch errors
|
||||
}
|
||||
}
|
||||
|
||||
function handleLicensePlateChange(licensePlate: string) {
|
||||
showMaintenanceReminders(licensePlate)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleMaintenanceApplyApi.list({
|
||||
q: searchForm.q || undefined,
|
||||
maintenanceType: searchForm.maintenanceType || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
applyDateFrom: dateRange.value?.[0],
|
||||
applyDateTo: dateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.maintenanceType = ''
|
||||
searchForm.status = ''
|
||||
dateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: VehicleMaintenanceApply[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.applyNo = ''
|
||||
formData.applicantId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.maintenanceType = ''
|
||||
formData.applyDate = ''
|
||||
formData.startDate = ''
|
||||
formData.expectedPickupDate = ''
|
||||
formData.maintenanceContent = ''
|
||||
formData.shopAddress = ''
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
resetForm()
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增申请'
|
||||
formData.applyDate = new Date().toISOString().slice(0, 10)
|
||||
formData.applicantId = currentUserId.value
|
||||
await loadUserOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleMaintenanceApply) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑申请'
|
||||
await loadUserOptions()
|
||||
Object.assign(formData, {
|
||||
id: row.id,
|
||||
applyNo: row.applyNo,
|
||||
applicantId: row.applicantId || '',
|
||||
licensePlate: row.licensePlate,
|
||||
maintenanceType: row.maintenanceType,
|
||||
applyDate: row.applyDate,
|
||||
startDate: row.startDate,
|
||||
expectedPickupDate: row.expectedPickupDate || '',
|
||||
maintenanceContent: row.maintenanceContent || '',
|
||||
shopAddress: row.shopAddress || '',
|
||||
attachmentUrls: row.attachmentUrls || '',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleView(row: VehicleMaintenanceApply) {
|
||||
viewVisible.value = true
|
||||
viewLoading.value = true
|
||||
try {
|
||||
const res = await vehicleMaintenanceApplyApi.get(row.id)
|
||||
viewDetail.value = res.data ?? res
|
||||
} finally {
|
||||
viewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applicantId: formData.applicantId,
|
||||
licensePlate: formData.licensePlate,
|
||||
maintenanceType: formData.maintenanceType,
|
||||
applyDate: formData.applyDate,
|
||||
startDate: formData.startDate,
|
||||
expectedPickupDate: formData.expectedPickupDate || undefined,
|
||||
maintenanceContent: formData.maintenanceContent || undefined,
|
||||
shopAddress: formData.shopAddress || undefined,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await vehicleMaintenanceApplyApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleMaintenanceApplyApi.create(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel(row: VehicleMaintenanceApply) {
|
||||
await ElMessageBox.confirm('确定作废该申请吗?', '提示', { type: 'warning' })
|
||||
cancelLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleMaintenanceApplyApi.cancel(row.id)
|
||||
ElMessage.success('已作废')
|
||||
fetchData()
|
||||
} finally {
|
||||
cancelLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleMaintenanceApply) {
|
||||
await ElMessageBox.confirm('确定删除该申请吗?', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleMaintenanceApplyApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadVehicleOptions(), loadUserOptions()])
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,586 @@
|
||||
<!-- 车辆维保处理登记 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<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.q" placeholder="搜索关键词" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="维保类型">
|
||||
<el-select
|
||||
v-model="searchForm.maintenanceType"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in PL_MAINTENANCE_TYPE_PRESETS"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请状态">
|
||||
<el-select v-model="searchForm.applyStatus" placeholder="全部" clearable>
|
||||
<el-option label="待处理" value="pending" />
|
||||
<el-option label="已处理" value="processed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="applyDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="处理日期">
|
||||
<el-date-picker
|
||||
v-model="processDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @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">{{ pagination.total }}</span> 条申请</div>
|
||||
<el-button @click="handleSupplement">
|
||||
<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="applyId">
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip fixed="left" />
|
||||
<el-table-column prop="applyDate" label="申请日期" min-width="120" align="center" />
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="applyStatus" label="申请状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlMaintenanceApplyStatusTagType(row.applyStatus)" size="small">
|
||||
{{ getPlMaintenanceApplyStatusLabel(row.applyStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="110" align="center" />
|
||||
<el-table-column prop="maintenanceType" label="维保类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlMaintenanceTypeTagType(row.maintenanceType)" size="small">
|
||||
{{ getPlMaintenanceTypeLabel(row.maintenanceType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startDate" label="开始日期" min-width="120" align="center" />
|
||||
<el-table-column prop="expectedPickupDate" label="预计取车" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.expectedPickupDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="processNo" label="处理单号" min-width="170" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.processNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="processDate" label="处理日期" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.processDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额(元)" min-width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.amount != null ? Number(row.amount).toFixed(2) : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="handlerName" label="处理人" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="260" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.applyStatus === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleProcess(row)"
|
||||
>
|
||||
<el-icon><Tools /></el-icon>处理
|
||||
</el-button>
|
||||
<template v-if="row.applyStatus === 'processed' && row.processId">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:loading="deleteLoadingMap[row.processId]"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>撤销处理
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" scroll-to-error>
|
||||
<div v-if="dialogMode === 'process' && currentApplyRow" class="apply-summary-block">
|
||||
<div class="apply-summary-title">关联申请</div>
|
||||
<div class="apply-summary-grid">
|
||||
<div><span class="label">申请单号</span>{{ currentApplyRow.applyNo }}</div>
|
||||
<div><span class="label">申请日期</span>{{ currentApplyRow.applyDate }}</div>
|
||||
<div><span class="label">车牌号</span>{{ currentApplyRow.licensePlate }}</div>
|
||||
<div>
|
||||
<span class="label">维保类型</span>
|
||||
{{ getPlMaintenanceTypeLabel(currentApplyRow.maintenanceType) }}
|
||||
</div>
|
||||
<div><span class="label">申请人</span>{{ currentApplyRow.applicantName || '—' }}</div>
|
||||
<div><span class="label">开始日期</span>{{ currentApplyRow.startDate }}</div>
|
||||
<div>
|
||||
<span class="label">预计取车</span>{{ currentApplyRow.expectedPickupDate || '—' }}
|
||||
</div>
|
||||
<div class="apply-summary-span-2">
|
||||
<span class="label">维保内容</span>{{ currentApplyRow.maintenanceContent || '—' }}
|
||||
</div>
|
||||
<div class="apply-summary-span-2">
|
||||
<span class="label">维修店地址</span>{{ currentApplyRow.shopAddress || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="处理单号">
|
||||
<el-input :model-value="formData.processNo || '保存后自动生成'" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="处理人" prop="handlerId">
|
||||
<el-select
|
||||
v-model="formData.handlerId"
|
||||
filterable
|
||||
placeholder="请选择处理人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.displayName"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="dialogMode === 'supplement'">
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
@change="handleLicensePlateChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="处理日期" prop="processDate">
|
||||
<el-date-picker v-model="formData.processDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="金额(元)">
|
||||
<el-input-number
|
||||
v-model="formData.amount"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
placeholder="可选"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-maintenance/process"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Tools } from '@element-plus/icons-vue'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleMaintenanceApplyApi,
|
||||
vehicleMaintenanceProcessApi,
|
||||
type Vehicle,
|
||||
type VehicleMaintenanceProcessBoardItem
|
||||
} from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
getPlMaintenanceApplyStatusLabel,
|
||||
getPlMaintenanceApplyStatusTagType,
|
||||
getPlMaintenanceTypeLabel,
|
||||
getPlMaintenanceTypeTagType,
|
||||
PL_MAINTENANCE_TYPE_PRESETS
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
type DialogMode = 'process' | 'edit' | 'supplement'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
})
|
||||
|
||||
const userOptions = ref<
|
||||
Array<{ userId: string; nickName: string; displayName: string }>
|
||||
>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleMaintenanceProcessBoardItem[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const applyDateRange = ref<string[]>([])
|
||||
const processDateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
maintenanceType: '',
|
||||
applyStatus: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const dialogMode = ref<DialogMode>('process')
|
||||
const currentApplyRow = ref<VehicleMaintenanceProcessBoardItem | null>(null)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
processNo: '',
|
||||
applyId: '',
|
||||
licensePlate: '',
|
||||
handlerId: '',
|
||||
processDate: '',
|
||||
amount: null as number | null,
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = computed(() => ({
|
||||
licensePlate:
|
||||
dialogMode.value === 'supplement'
|
||||
? [{ required: true, message: '请选择车牌号', trigger: 'change' }]
|
||||
: [],
|
||||
handlerId: [{ required: true, message: '请选择处理人', trigger: 'change' }],
|
||||
processDate: [{ required: true, message: '请选择处理日期', trigger: 'change' }]
|
||||
}))
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: unknown = (res as { data?: unknown })?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: Record<string, unknown>) => {
|
||||
const name = String(u.label || u.nickName || '')
|
||||
const account = String(u.userNo || '')
|
||||
return {
|
||||
userId: String(u.value ?? u.id ?? ''),
|
||||
nickName: name,
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list()
|
||||
vehicleOptions.value = (res.data ?? res) || []
|
||||
}
|
||||
|
||||
async function showMaintenanceReminders(licensePlate: string) {
|
||||
if (!licensePlate) return
|
||||
try {
|
||||
const res = await vehicleMaintenanceApplyApi.reminders(licensePlate)
|
||||
const items = (res.data ?? res) || []
|
||||
if (!items.length) return
|
||||
const text = items
|
||||
.map((item, index) => {
|
||||
const lines = [
|
||||
`【${index + 1}】申请单号:${item.applyNo}`,
|
||||
`维保类型:${item.maintenanceType}`,
|
||||
`开始日期:${item.startDate}`,
|
||||
`预计取车:${item.expectedPickupDate || '—'}`,
|
||||
`维保内容:${item.maintenanceContent || '—'}`,
|
||||
`维修店地址:${item.shopAddress || '—'}`
|
||||
]
|
||||
return lines.join('\n')
|
||||
})
|
||||
.join('\n\n')
|
||||
await ElMessageBox.alert(text, '车辆维保提醒', { confirmButtonText: '知道了' })
|
||||
} catch {
|
||||
// ignore reminder fetch errors
|
||||
}
|
||||
}
|
||||
|
||||
function handleLicensePlateChange(licensePlate: string) {
|
||||
showMaintenanceReminders(licensePlate)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleMaintenanceProcessApi.processBoard({
|
||||
q: searchForm.q || undefined,
|
||||
maintenanceType: searchForm.maintenanceType || undefined,
|
||||
applyStatus: searchForm.applyStatus || undefined,
|
||||
applyDateFrom: applyDateRange.value?.[0],
|
||||
applyDateTo: applyDateRange.value?.[1],
|
||||
processDateFrom: processDateRange.value?.[0],
|
||||
processDateTo: processDateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.maintenanceType = ''
|
||||
searchForm.applyStatus = ''
|
||||
applyDateRange.value = []
|
||||
processDateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.processNo = ''
|
||||
formData.applyId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.handlerId = ''
|
||||
formData.processDate = ''
|
||||
formData.amount = null
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
currentApplyRow.value = null
|
||||
}
|
||||
|
||||
async function openDialogWithDefaults(mode: DialogMode, title: string) {
|
||||
dialogMode.value = mode
|
||||
dialogTitle.value = title
|
||||
formData.processDate = new Date().toISOString().slice(0, 10)
|
||||
formData.handlerId = currentUserId.value
|
||||
await loadUserOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleProcess(row: VehicleMaintenanceProcessBoardItem) {
|
||||
resetForm()
|
||||
currentApplyRow.value = row
|
||||
formData.applyId = row.applyId
|
||||
formData.licensePlate = row.licensePlate
|
||||
await openDialogWithDefaults('process', '处理登记')
|
||||
}
|
||||
|
||||
async function handleSupplement() {
|
||||
resetForm()
|
||||
await loadVehicleOptions()
|
||||
await openDialogWithDefaults('supplement', '无申请补录')
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleMaintenanceProcessBoardItem) {
|
||||
if (!row.processId) return
|
||||
resetForm()
|
||||
dialogMode.value = 'edit'
|
||||
dialogTitle.value = '编辑处理登记'
|
||||
currentApplyRow.value = row
|
||||
await loadUserOptions()
|
||||
const res = await vehicleMaintenanceProcessApi.get(row.processId)
|
||||
const detail = res.data ?? res
|
||||
Object.assign(formData, {
|
||||
id: detail.id,
|
||||
processNo: detail.processNo,
|
||||
applyId: detail.applyId || row.applyId,
|
||||
licensePlate: detail.licensePlate || row.licensePlate,
|
||||
handlerId: detail.handlerId,
|
||||
processDate: detail.processDate,
|
||||
amount: detail.amount != null ? Number(detail.amount) : null,
|
||||
attachmentUrls: detail.attachmentUrls || '',
|
||||
remark: detail.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applyId: formData.applyId || undefined,
|
||||
licensePlate: dialogMode.value === 'supplement' ? formData.licensePlate : undefined,
|
||||
handlerId: formData.handlerId,
|
||||
processDate: formData.processDate,
|
||||
amount: formData.amount ?? undefined,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (dialogMode.value === 'edit') {
|
||||
await vehicleMaintenanceProcessApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleMaintenanceProcessApi.create(payload)
|
||||
ElMessage.success('处理登记成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleMaintenanceProcessBoardItem) {
|
||||
if (!row.processId) return
|
||||
await ElMessageBox.confirm('确定撤销该处理登记吗?关联申请将回退为待处理。', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.processId] = true
|
||||
try {
|
||||
await vehicleMaintenanceProcessApi.delete(row.processId)
|
||||
ElMessage.success('已撤销处理')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.processId] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.apply-summary-block {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--primary-bg);
|
||||
}
|
||||
|
||||
.apply-summary-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.apply-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
min-width: 72px;
|
||||
margin-right: 6px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.apply-summary-span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,508 @@
|
||||
<!-- ETC加油申请 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">ETC加油申请</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.q" placeholder="搜索申请单号或车牌" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="充值类型">
|
||||
<el-select v-model="searchForm.rechargeType" placeholder="全部" clearable>
|
||||
<el-option label="ETC" value="etc" />
|
||||
<el-option label="加油" value="fuel" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||||
<el-option label="待发放" value="pending" />
|
||||
<el-option label="已发放" value="disbursed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @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"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="rechargeType" label="充值类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlRechargeTypeTagType(row.rechargeType)" size="small">
|
||||
{{ getPlRechargeTypeLabel(row.rechargeType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyDate" label="申请日期" min-width="120" align="center" />
|
||||
<el-table-column prop="odometerKm" label="期初公里数" min-width="110" align="center">
|
||||
<template #default="{ row }">{{ row.odometerKm ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="status" label="状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlRechargeApplyStatusTagType(row.status)" size="small">
|
||||
{{ getPlRechargeApplyStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<template v-if="row.status === 'pending'">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="warning" :loading="cancelLoadingMap[row.id]" @click="handleCancel(row)">
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button link type="danger" :loading="deleteLoadingMap[row.id]" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item v-if="formData.applyNo" label="申请单号">
|
||||
<el-input v-model="formData.applyNo" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请人" prop="applicantId">
|
||||
<el-select
|
||||
v-model="formData.applicantId"
|
||||
filterable
|
||||
placeholder="请选择申请人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.displayName"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select v-model="formData.licensePlate" filterable placeholder="请选择车牌" style="width: 100%">
|
||||
<el-option v-for="item in vehicleOptions" :key="item.id" :label="item.licensePlate" :value="item.licensePlate" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值类型" prop="rechargeType">
|
||||
<el-select v-model="formData.rechargeType" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="ETC" value="etc" />
|
||||
<el-option label="加油" value="fuel" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期" prop="applyDate">
|
||||
<el-date-picker v-model="formData.applyDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="期初公里数" prop="odometerKm">
|
||||
<el-input-number v-model="formData.odometerKm" :min="0" :precision="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-recharge/apply"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewDetail?.applyNo || '申请详情'"
|
||||
:sections="viewSections"
|
||||
:loading="viewLoading"
|
||||
width="720px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Check,
|
||||
CircleClose,
|
||||
Close,
|
||||
Delete,
|
||||
Edit,
|
||||
Plus,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleRechargeApplyApi,
|
||||
type Vehicle,
|
||||
type VehicleRechargeApply
|
||||
} from '@/api/pl'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
getPlRechargeApplyStatusLabel,
|
||||
getPlRechargeApplyStatusTagType,
|
||||
getPlRechargeTypeLabel,
|
||||
getPlRechargeTypeTagType
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
})
|
||||
|
||||
const userOptions = ref<
|
||||
Array<{ userId: string; nickName: string; displayName: string }>
|
||||
>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const cancelLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleRechargeApply[]>([])
|
||||
const selectedRows = ref<VehicleRechargeApply[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
rechargeType: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
applyNo: '',
|
||||
applicantId: '',
|
||||
licensePlate: '',
|
||||
rechargeType: 'etc' as 'etc' | 'fuel',
|
||||
applyDate: '',
|
||||
odometerKm: null as number | null,
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
applicantId: [{ required: true, message: '请选择申请人', trigger: 'change' }],
|
||||
licensePlate: [{ required: true, message: '请选择车牌号', trigger: 'change' }],
|
||||
rechargeType: [{ required: true, message: '请选择充值类型', trigger: 'change' }],
|
||||
applyDate: [{ required: true, message: '请选择申请日期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewLoading = ref(false)
|
||||
const viewDetail = ref<VehicleRechargeApply | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
return [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: d.applyNo },
|
||||
{ label: '车牌号', value: d.licensePlate },
|
||||
{
|
||||
label: '充值类型',
|
||||
value: getPlRechargeTypeLabel(d.rechargeType),
|
||||
tagType: getPlRechargeTypeTagType(d.rechargeType)
|
||||
},
|
||||
{ label: '申请日期', value: d.applyDate },
|
||||
{ label: '期初公里数', value: d.odometerKm != null ? String(d.odometerKm) : '—' },
|
||||
{ label: '申请人', value: d.applicantName || '—' },
|
||||
{
|
||||
label: '状态',
|
||||
value: getPlRechargeApplyStatusLabel(d.status),
|
||||
tagType: getPlRechargeApplyStatusTagType(d.status)
|
||||
},
|
||||
{ label: '备注', value: d.remark || '—', span: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: unknown = (res as { data?: unknown })?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: Record<string, unknown>) => {
|
||||
const name = String(u.label || u.nickName || '')
|
||||
const account = String(u.userNo || '')
|
||||
return {
|
||||
userId: String(u.value ?? u.id ?? ''),
|
||||
nickName: name,
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list()
|
||||
vehicleOptions.value = (res.data ?? res) || []
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleRechargeApplyApi.list({
|
||||
q: searchForm.q || undefined,
|
||||
rechargeType: searchForm.rechargeType || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
applyDateFrom: dateRange.value?.[0],
|
||||
applyDateTo: dateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.rechargeType = ''
|
||||
searchForm.status = ''
|
||||
dateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: VehicleRechargeApply[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.applyNo = ''
|
||||
formData.applicantId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.rechargeType = 'etc'
|
||||
formData.applyDate = ''
|
||||
formData.odometerKm = null
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
resetForm()
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增申请'
|
||||
formData.applyDate = new Date().toISOString().slice(0, 10)
|
||||
formData.applicantId = currentUserId.value
|
||||
await loadUserOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleRechargeApply) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑申请'
|
||||
await loadUserOptions()
|
||||
Object.assign(formData, {
|
||||
id: row.id,
|
||||
applyNo: row.applyNo,
|
||||
applicantId: row.applicantId || '',
|
||||
licensePlate: row.licensePlate,
|
||||
rechargeType: row.rechargeType,
|
||||
applyDate: row.applyDate,
|
||||
odometerKm: row.odometerKm ?? null,
|
||||
attachmentUrls: row.attachmentUrls || '',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleView(row: VehicleRechargeApply) {
|
||||
viewVisible.value = true
|
||||
viewLoading.value = true
|
||||
try {
|
||||
const res = await vehicleRechargeApplyApi.get(row.id)
|
||||
viewDetail.value = res.data ?? res
|
||||
} finally {
|
||||
viewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applicantId: formData.applicantId,
|
||||
licensePlate: formData.licensePlate,
|
||||
rechargeType: formData.rechargeType,
|
||||
applyDate: formData.applyDate,
|
||||
odometerKm: formData.odometerKm,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await vehicleRechargeApplyApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleRechargeApplyApi.create(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel(row: VehicleRechargeApply) {
|
||||
await ElMessageBox.confirm('确定作废该申请吗?', '提示', { type: 'warning' })
|
||||
cancelLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleRechargeApplyApi.cancel(row.id)
|
||||
ElMessage.success('已作废')
|
||||
fetchData()
|
||||
} finally {
|
||||
cancelLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleRechargeApply) {
|
||||
await ElMessageBox.confirm('确定删除该申请吗?', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleRechargeApplyApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadVehicleOptions(), loadUserOptions()])
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,604 @@
|
||||
<!-- ETC加油发放登记 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">ETC加油发放登记</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.q" placeholder="搜索关键词" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="充值类型">
|
||||
<el-select v-model="searchForm.rechargeType" placeholder="全部" clearable>
|
||||
<el-option label="ETC" value="etc" />
|
||||
<el-option label="加油" value="fuel" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请状态">
|
||||
<el-select v-model="searchForm.applyStatus" placeholder="全部" clearable>
|
||||
<el-option label="待发放" value="pending" />
|
||||
<el-option label="已发放" value="disbursed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="applyDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值日期">
|
||||
<el-date-picker
|
||||
v-model="rechargeDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @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">{{ pagination.total }}</span> 条申请</div>
|
||||
<el-button @click="handleSupplement">
|
||||
<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="applyId">
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip fixed="left" />
|
||||
<el-table-column prop="applyDate" label="申请日期" min-width="120" align="center" />
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="odometerKm" label="期初公里数" min-width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.odometerKm != null ? Number(row.odometerKm).toFixed(1) : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyStatus" label="申请状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlRechargeApplyStatusTagType(row.applyStatus)" size="small">
|
||||
{{ getPlRechargeApplyStatusLabel(row.applyStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="110" align="center" />
|
||||
<el-table-column prop="rechargeType" label="充值类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlRechargeTypeTagType(row.rechargeType)" size="small">
|
||||
{{ getPlRechargeTypeLabel(row.rechargeType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rechargeOrderNo" label="充值订单号" min-width="170" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.rechargeOrderNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rechargeDate" label="充值日期" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.rechargeDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cardNo" label="卡号" min-width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.cardNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="gasStation" label="加油站/渠道" min-width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.gasStation || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额(元)" min-width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.amount != null ? Number(row.amount).toFixed(2) : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="recordCreateByName" label="登记人" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ row.recordCreateByName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="300" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.applyStatus === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDisburse(row)"
|
||||
>
|
||||
<el-icon><Money /></el-icon>发放
|
||||
</el-button>
|
||||
<template v-if="row.applyStatus === 'disbursed' && row.recordId">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:loading="deleteLoadingMap[row.recordId]"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>撤销发放
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" scroll-to-error>
|
||||
<div v-if="dialogMode === 'disburse' && currentApplyRow" class="apply-summary-block">
|
||||
<div class="apply-summary-title">关联申请</div>
|
||||
<div class="apply-summary-grid">
|
||||
<div><span class="label">申请单号</span>{{ currentApplyRow.applyNo }}</div>
|
||||
<div><span class="label">申请日期</span>{{ currentApplyRow.applyDate }}</div>
|
||||
<div><span class="label">车牌号</span>{{ currentApplyRow.licensePlate }}</div>
|
||||
<div>
|
||||
<span class="label">充值类型</span>
|
||||
{{ getPlRechargeTypeLabel(currentApplyRow.rechargeType) }}
|
||||
</div>
|
||||
<div><span class="label">申请人</span>{{ currentApplyRow.applicantName || '—' }}</div>
|
||||
<div>
|
||||
<span class="label">期初公里数</span>
|
||||
{{
|
||||
currentApplyRow.odometerKm != null
|
||||
? `${Number(currentApplyRow.odometerKm).toFixed(1)} km`
|
||||
: '—'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="充值订单号">
|
||||
<el-input :model-value="formData.rechargeOrderNo || '保存后自动生成'" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="登记人">
|
||||
<el-input :model-value="currentUserName" disabled />
|
||||
</el-form-item>
|
||||
<template v-if="dialogMode === 'supplement'">
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值类型" prop="rechargeType">
|
||||
<el-select v-model="formData.rechargeType" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="ETC" value="etc" />
|
||||
<el-option label="加油" value="fuel" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="充值日期" prop="rechargeDate">
|
||||
<el-date-picker v-model="formData.rechargeDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="卡号" prop="cardNo">
|
||||
<el-input v-model="formData.cardNo" placeholder="ETC卡号/油卡号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="加油站/渠道" prop="gasStation">
|
||||
<el-select
|
||||
v-model="formData.gasStation"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in PL_GAS_STATION_PRESETS" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值金额(元)" prop="amount">
|
||||
<el-input-number v-model="formData.amount" :min="0.01" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-recharge/record"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewRow?.applyNo || '发放详情'"
|
||||
:sections="viewSections"
|
||||
width="760px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Edit, Money, Plus, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleRechargeRecordApi,
|
||||
type Vehicle,
|
||||
type VehicleRechargeDisburseBoardItem
|
||||
} from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
getPlRechargeApplyStatusLabel,
|
||||
getPlRechargeApplyStatusTagType,
|
||||
getPlRechargeTypeLabel,
|
||||
getPlRechargeTypeTagType,
|
||||
PL_GAS_STATION_PRESETS
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
type DialogMode = 'disburse' | 'edit' | 'supplement'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const currentUserName = computed(() => userStore.info?.userName || '—')
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleRechargeDisburseBoardItem[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const applyDateRange = ref<string[]>([])
|
||||
const rechargeDateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
rechargeType: '',
|
||||
applyStatus: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const dialogMode = ref<DialogMode>('disburse')
|
||||
const currentApplyRow = ref<VehicleRechargeDisburseBoardItem | null>(null)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
rechargeOrderNo: '',
|
||||
applyId: '',
|
||||
licensePlate: '',
|
||||
rechargeType: 'etc' as 'etc' | 'fuel',
|
||||
rechargeDate: '',
|
||||
cardNo: '',
|
||||
gasStation: '',
|
||||
amount: null as number | null,
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = computed(() => ({
|
||||
licensePlate:
|
||||
dialogMode.value === 'supplement'
|
||||
? [{ required: true, message: '请选择车牌号', trigger: 'change' }]
|
||||
: [],
|
||||
rechargeType:
|
||||
dialogMode.value === 'supplement'
|
||||
? [{ required: true, message: '请选择充值类型', trigger: 'change' }]
|
||||
: [],
|
||||
rechargeDate: [{ required: true, message: '请选择充值日期', trigger: 'change' }],
|
||||
amount: [{ required: true, message: '请输入充值金额', trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewRow = ref<VehicleRechargeDisburseBoardItem | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const row = viewRow.value
|
||||
if (!row) return []
|
||||
const sections: ViewSection[] = [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: row.applyNo },
|
||||
{ label: '申请日期', value: row.applyDate },
|
||||
{
|
||||
label: '申请状态',
|
||||
value: getPlRechargeApplyStatusLabel(row.applyStatus),
|
||||
tagType: getPlRechargeApplyStatusTagType(row.applyStatus)
|
||||
},
|
||||
{ label: '车牌号', value: row.licensePlate },
|
||||
{
|
||||
label: '充值类型',
|
||||
value: getPlRechargeTypeLabel(row.rechargeType),
|
||||
tagType: getPlRechargeTypeTagType(row.rechargeType)
|
||||
},
|
||||
{ label: '申请人', value: row.applicantName || '—' },
|
||||
{
|
||||
label: '期初公里数',
|
||||
value: row.odometerKm != null ? `${Number(row.odometerKm).toFixed(1)} km` : '—'
|
||||
},
|
||||
{ label: '申请备注', value: row.applyRemark || '—', span: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
if (row.recordId) {
|
||||
sections.push({
|
||||
title: '发放信息',
|
||||
fields: [
|
||||
{ label: '充值订单号', value: row.rechargeOrderNo || '—' },
|
||||
{ label: '充值日期', value: row.rechargeDate || '—' },
|
||||
{ label: '卡号', value: row.cardNo || '—' },
|
||||
{ label: '加油站/渠道', value: row.gasStation || '—' },
|
||||
{
|
||||
label: '充值金额',
|
||||
value: row.amount != null ? `${Number(row.amount).toFixed(2)} 元` : '—'
|
||||
},
|
||||
{ label: '登记人', value: row.recordCreateByName || '—' },
|
||||
{ label: '登记备注', value: row.recordRemark || '—', span: 2 }
|
||||
]
|
||||
})
|
||||
}
|
||||
return sections
|
||||
})
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const vehicles = await vehicleApi.list()
|
||||
vehicleOptions.value = (vehicles.data ?? vehicles) || []
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleRechargeRecordApi.disburseBoard({
|
||||
q: searchForm.q || undefined,
|
||||
rechargeType: searchForm.rechargeType || undefined,
|
||||
applyStatus: searchForm.applyStatus || undefined,
|
||||
applyDateFrom: applyDateRange.value?.[0],
|
||||
applyDateTo: applyDateRange.value?.[1],
|
||||
rechargeDateFrom: rechargeDateRange.value?.[0],
|
||||
rechargeDateTo: rechargeDateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.rechargeType = ''
|
||||
searchForm.applyStatus = ''
|
||||
applyDateRange.value = []
|
||||
rechargeDateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.rechargeOrderNo = ''
|
||||
formData.applyId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.rechargeType = 'etc'
|
||||
formData.rechargeDate = ''
|
||||
formData.cardNo = ''
|
||||
formData.gasStation = ''
|
||||
formData.amount = null
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
currentApplyRow.value = null
|
||||
}
|
||||
|
||||
function handleDisburse(row: VehicleRechargeDisburseBoardItem) {
|
||||
resetForm()
|
||||
dialogMode.value = 'disburse'
|
||||
dialogTitle.value = '发放登记'
|
||||
currentApplyRow.value = row
|
||||
formData.applyId = row.applyId
|
||||
formData.licensePlate = row.licensePlate
|
||||
formData.rechargeType = row.rechargeType
|
||||
formData.rechargeDate = new Date().toISOString().slice(0, 10)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSupplement() {
|
||||
resetForm()
|
||||
dialogMode.value = 'supplement'
|
||||
dialogTitle.value = '无申请补录'
|
||||
formData.rechargeDate = new Date().toISOString().slice(0, 10)
|
||||
await loadVehicleOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleRechargeDisburseBoardItem) {
|
||||
if (!row.recordId) return
|
||||
resetForm()
|
||||
dialogMode.value = 'edit'
|
||||
dialogTitle.value = '编辑发放登记'
|
||||
currentApplyRow.value = row
|
||||
const res = await vehicleRechargeRecordApi.get(row.recordId)
|
||||
const detail = res.data ?? res
|
||||
Object.assign(formData, {
|
||||
id: detail.id,
|
||||
rechargeOrderNo: detail.rechargeOrderNo,
|
||||
applyId: detail.applyId || row.applyId,
|
||||
licensePlate: detail.licensePlate || row.licensePlate,
|
||||
rechargeType: detail.rechargeType,
|
||||
rechargeDate: detail.rechargeDate,
|
||||
cardNo: detail.cardNo || '',
|
||||
gasStation: detail.gasStation || '',
|
||||
amount: Number(detail.amount),
|
||||
attachmentUrls: detail.attachmentUrls || '',
|
||||
remark: detail.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleView(row: VehicleRechargeDisburseBoardItem) {
|
||||
viewRow.value = row
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applyId: formData.applyId || undefined,
|
||||
licensePlate: dialogMode.value === 'supplement' ? formData.licensePlate : undefined,
|
||||
rechargeType:
|
||||
dialogMode.value === 'supplement'
|
||||
? formData.rechargeType
|
||||
: dialogMode.value === 'edit'
|
||||
? formData.rechargeType
|
||||
: currentApplyRow.value?.rechargeType,
|
||||
rechargeDate: formData.rechargeDate,
|
||||
cardNo: formData.cardNo || undefined,
|
||||
gasStation: formData.gasStation || undefined,
|
||||
amount: formData.amount,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (dialogMode.value === 'edit') {
|
||||
await vehicleRechargeRecordApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleRechargeRecordApi.create(payload)
|
||||
ElMessage.success('发放登记成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleRechargeDisburseBoardItem) {
|
||||
if (!row.recordId) return
|
||||
await ElMessageBox.confirm('确定撤销该发放登记吗?关联申请将回退为待发放。', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.recordId] = true
|
||||
try {
|
||||
await vehicleRechargeRecordApi.delete(row.recordId)
|
||||
ElMessage.success('已撤销发放')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.recordId] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.apply-summary-block {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--primary-bg);
|
||||
}
|
||||
|
||||
.apply-summary-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.apply-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
min-width: 72px;
|
||||
margin-right: 6px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,523 @@
|
||||
<!-- 车辆尿素申请 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<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.q" placeholder="搜索申请单号或车牌" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||||
<el-option label="待发放" value="pending" />
|
||||
<el-option label="已发放" value="disbursed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @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"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="applyDate" label="申请时间" min-width="120" align="center" />
|
||||
<el-table-column prop="warehouseInDate" label="入库时间" min-width="120" align="center" />
|
||||
<el-table-column prop="unit" label="单位" min-width="90" align="center" />
|
||||
<el-table-column prop="quantity" label="入库数" min-width="100" align="right">
|
||||
<template #default="{ row }">{{ Number(row.quantity).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyAmount" label="申请金额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ Number(row.applyAmount).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="status" label="状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlUreaApplyStatusTagType(row.status)" size="small">
|
||||
{{ getPlUreaApplyStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<template v-if="row.status === 'pending'">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="warning" :loading="cancelLoadingMap[row.id]" @click="handleCancel(row)">
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button link type="danger" :loading="deleteLoadingMap[row.id]" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item v-if="formData.applyNo" label="申请单号">
|
||||
<el-input v-model="formData.applyNo" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请人" prop="applicantId">
|
||||
<el-select
|
||||
v-model="formData.applicantId"
|
||||
filterable
|
||||
placeholder="请选择申请人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.displayName"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select v-model="formData.licensePlate" filterable placeholder="请选择车牌" style="width: 100%">
|
||||
<el-option v-for="item in vehicleOptions" :key="item.id" :label="item.licensePlate" :value="item.licensePlate" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请时间" prop="applyDate">
|
||||
<el-date-picker v-model="formData.applyDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="入库时间" prop="warehouseInDate">
|
||||
<el-date-picker v-model="formData.warehouseInDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位" prop="unit">
|
||||
<el-select
|
||||
v-model="formData.unit"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in PL_UREA_UNIT_PRESETS" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="入库数" prop="quantity">
|
||||
<el-input-number v-model="formData.quantity" :min="0.01" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请金额(元)" prop="applyAmount">
|
||||
<el-input-number v-model="formData.applyAmount" :min="0.01" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-urea/apply"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewDetail?.applyNo || '申请详情'"
|
||||
:sections="viewSections"
|
||||
:loading="viewLoading"
|
||||
width="720px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Check,
|
||||
CircleClose,
|
||||
Close,
|
||||
Delete,
|
||||
Edit,
|
||||
Plus,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleUreaApplyApi,
|
||||
type Vehicle,
|
||||
type VehicleUreaApply
|
||||
} from '@/api/pl'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
getPlUreaApplyStatusLabel,
|
||||
getPlUreaApplyStatusTagType,
|
||||
PL_UREA_UNIT_PRESETS
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
})
|
||||
|
||||
const userOptions = ref<
|
||||
Array<{ userId: string; nickName: string; displayName: string }>
|
||||
>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const cancelLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleUreaApply[]>([])
|
||||
const selectedRows = ref<VehicleUreaApply[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
applyNo: '',
|
||||
applicantId: '',
|
||||
licensePlate: '',
|
||||
applyDate: '',
|
||||
warehouseInDate: '',
|
||||
unit: '吨桶',
|
||||
quantity: null as number | null,
|
||||
applyAmount: null as number | null,
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
applicantId: [{ required: true, message: '请选择申请人', trigger: 'change' }],
|
||||
licensePlate: [{ required: true, message: '请选择车牌号', trigger: 'change' }],
|
||||
applyDate: [{ required: true, message: '请选择申请时间', trigger: 'change' }],
|
||||
warehouseInDate: [{ required: true, message: '请选择入库时间', trigger: 'change' }],
|
||||
unit: [{ required: true, message: '请填写单位', trigger: 'change' }],
|
||||
quantity: [{ required: true, message: '请填写入库数', trigger: 'blur' }],
|
||||
applyAmount: [{ required: true, message: '请填写申请金额', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewLoading = ref(false)
|
||||
const viewDetail = ref<VehicleUreaApply | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
return [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: d.applyNo },
|
||||
{ label: '车牌号', value: d.licensePlate },
|
||||
{ label: '申请时间', value: d.applyDate },
|
||||
{ label: '入库时间', value: d.warehouseInDate },
|
||||
{ label: '单位', value: d.unit },
|
||||
{ label: '入库数', value: Number(d.quantity).toFixed(2) },
|
||||
{ label: '申请金额', value: `${Number(d.applyAmount).toFixed(2)} 元` },
|
||||
{ label: '申请人', value: d.applicantName || '—' },
|
||||
{
|
||||
label: '状态',
|
||||
value: getPlUreaApplyStatusLabel(d.status),
|
||||
tagType: getPlUreaApplyStatusTagType(d.status)
|
||||
},
|
||||
{ label: '备注', value: d.remark || '—', span: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: unknown = (res as { data?: unknown })?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: Record<string, unknown>) => {
|
||||
const name = String(u.label || u.nickName || '')
|
||||
const account = String(u.userNo || '')
|
||||
return {
|
||||
userId: String(u.value ?? u.id ?? ''),
|
||||
nickName: name,
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list()
|
||||
vehicleOptions.value = (res.data ?? res) || []
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleUreaApplyApi.list({
|
||||
q: searchForm.q || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
applyDateFrom: dateRange.value?.[0],
|
||||
applyDateTo: dateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.status = ''
|
||||
dateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: VehicleUreaApply[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.applyNo = ''
|
||||
formData.applicantId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.applyDate = ''
|
||||
formData.warehouseInDate = ''
|
||||
formData.unit = '吨桶'
|
||||
formData.quantity = null
|
||||
formData.applyAmount = null
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
resetForm()
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增申请'
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
formData.applyDate = today
|
||||
formData.warehouseInDate = today
|
||||
formData.applicantId = currentUserId.value
|
||||
await loadUserOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleUreaApply) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑申请'
|
||||
await loadUserOptions()
|
||||
Object.assign(formData, {
|
||||
id: row.id,
|
||||
applyNo: row.applyNo,
|
||||
applicantId: row.applicantId || '',
|
||||
licensePlate: row.licensePlate,
|
||||
applyDate: row.applyDate,
|
||||
warehouseInDate: row.warehouseInDate,
|
||||
unit: row.unit || '吨桶',
|
||||
quantity: Number(row.quantity),
|
||||
applyAmount: Number(row.applyAmount),
|
||||
attachmentUrls: row.attachmentUrls || '',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleView(row: VehicleUreaApply) {
|
||||
viewVisible.value = true
|
||||
viewLoading.value = true
|
||||
try {
|
||||
const res = await vehicleUreaApplyApi.get(row.id)
|
||||
viewDetail.value = res.data ?? res
|
||||
} finally {
|
||||
viewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (formData.applyDate && formData.warehouseInDate && formData.applyDate > formData.warehouseInDate) {
|
||||
ElMessage.warning('申请时间不能晚于入库时间')
|
||||
return
|
||||
}
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applicantId: formData.applicantId,
|
||||
licensePlate: formData.licensePlate,
|
||||
applyDate: formData.applyDate,
|
||||
warehouseInDate: formData.warehouseInDate,
|
||||
unit: formData.unit,
|
||||
quantity: formData.quantity,
|
||||
applyAmount: formData.applyAmount,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await vehicleUreaApplyApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleUreaApplyApi.create(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel(row: VehicleUreaApply) {
|
||||
await ElMessageBox.confirm('确定作废该申请吗?', '提示', { type: 'warning' })
|
||||
cancelLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleUreaApplyApi.cancel(row.id)
|
||||
ElMessage.success('已作废')
|
||||
fetchData()
|
||||
} finally {
|
||||
cancelLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleUreaApply) {
|
||||
await ElMessageBox.confirm('确定删除该申请吗?', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleUreaApplyApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadVehicleOptions(), loadUserOptions()])
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,573 @@
|
||||
<!-- 车辆尿素发放登记 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<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.q" placeholder="搜索关键词" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请状态">
|
||||
<el-select v-model="searchForm.applyStatus" placeholder="全部" clearable>
|
||||
<el-option label="待发放" value="pending" />
|
||||
<el-option label="已发放" value="disbursed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="applyDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="发放日期">
|
||||
<el-date-picker
|
||||
v-model="disburseDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @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">{{ pagination.total }}</span> 条申请</div>
|
||||
<el-button @click="handleSupplement">
|
||||
<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="applyId">
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip fixed="left" />
|
||||
<el-table-column prop="applyDate" label="申请时间" min-width="120" align="center" />
|
||||
<el-table-column prop="warehouseInDate" label="入库时间" min-width="120" align="center" />
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="unit" label="单位" min-width="90" align="center" />
|
||||
<el-table-column prop="quantity" label="入库数" min-width="100" align="right">
|
||||
<template #default="{ row }">{{ Number(row.quantity).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyAmount" label="申请金额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ Number(row.applyAmount).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyStatus" label="申请状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlUreaApplyStatusTagType(row.applyStatus)" size="small">
|
||||
{{ getPlUreaApplyStatusLabel(row.applyStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="110" align="center" />
|
||||
<el-table-column prop="disburseNo" label="发放单号" min-width="170" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.disburseNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="disburseDate" label="发放时间" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.disburseDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="发放金额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.amount != null ? Number(row.amount).toFixed(2) : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="handlerName" label="处理人" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="300" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.applyStatus === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDisburse(row)"
|
||||
>
|
||||
<el-icon><Money /></el-icon>发放
|
||||
</el-button>
|
||||
<template v-if="row.applyStatus === 'disbursed' && row.disburseId">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:loading="deleteLoadingMap[row.disburseId]"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>撤销发放
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" scroll-to-error>
|
||||
<div v-if="dialogMode === 'disburse' && currentApplyRow" class="apply-summary-block">
|
||||
<div class="apply-summary-title">关联申请</div>
|
||||
<div class="apply-summary-grid">
|
||||
<div><span class="label">申请单号</span>{{ currentApplyRow.applyNo }}</div>
|
||||
<div><span class="label">申请时间</span>{{ currentApplyRow.applyDate }}</div>
|
||||
<div><span class="label">车牌号</span>{{ currentApplyRow.licensePlate }}</div>
|
||||
<div><span class="label">入库时间</span>{{ currentApplyRow.warehouseInDate }}</div>
|
||||
<div><span class="label">单位</span>{{ currentApplyRow.unit }}</div>
|
||||
<div><span class="label">入库数</span>{{ Number(currentApplyRow.quantity).toFixed(2) }}</div>
|
||||
<div><span class="label">申请人</span>{{ currentApplyRow.applicantName || '—' }}</div>
|
||||
<div>
|
||||
<span class="label">申请金额</span>
|
||||
{{ Number(currentApplyRow.applyAmount).toFixed(2) }} 元
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="发放单号">
|
||||
<el-input :model-value="formData.disburseNo || '保存后自动生成'" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="处理人" prop="handlerId">
|
||||
<el-select
|
||||
v-model="formData.handlerId"
|
||||
filterable
|
||||
placeholder="请选择处理人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.displayName"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="dialogMode === 'supplement'">
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="发放时间" prop="disburseDate">
|
||||
<el-date-picker v-model="formData.disburseDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发放金额(元)" prop="amount">
|
||||
<el-input-number v-model="formData.amount" :min="0.01" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-urea/disburse"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewRow?.applyNo || '发放详情'"
|
||||
:sections="viewSections"
|
||||
width="760px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Edit, Money, Plus, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleUreaDisburseApi,
|
||||
type Vehicle,
|
||||
type VehicleUreaDisburseBoardItem
|
||||
} from '@/api/pl'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { getPlUreaApplyStatusLabel, getPlUreaApplyStatusTagType } from '@/utils/pl-display'
|
||||
|
||||
type DialogMode = 'disburse' | 'edit' | 'supplement'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
})
|
||||
|
||||
const userOptions = ref<
|
||||
Array<{ userId: string; nickName: string; displayName: string }>
|
||||
>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleUreaDisburseBoardItem[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const applyDateRange = ref<string[]>([])
|
||||
const disburseDateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
applyStatus: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const dialogMode = ref<DialogMode>('disburse')
|
||||
const currentApplyRow = ref<VehicleUreaDisburseBoardItem | null>(null)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
disburseNo: '',
|
||||
applyId: '',
|
||||
licensePlate: '',
|
||||
handlerId: '',
|
||||
disburseDate: '',
|
||||
amount: null as number | null,
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = computed(() => ({
|
||||
licensePlate:
|
||||
dialogMode.value === 'supplement'
|
||||
? [{ required: true, message: '请选择车牌号', trigger: 'change' }]
|
||||
: [],
|
||||
handlerId: [{ required: true, message: '请选择处理人', trigger: 'change' }],
|
||||
disburseDate: [{ required: true, message: '请选择发放时间', trigger: 'change' }],
|
||||
amount: [{ required: true, message: '请输入发放金额', trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewRow = ref<VehicleUreaDisburseBoardItem | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const row = viewRow.value
|
||||
if (!row) return []
|
||||
const sections: ViewSection[] = [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: row.applyNo },
|
||||
{ label: '申请时间', value: row.applyDate },
|
||||
{ label: '入库时间', value: row.warehouseInDate },
|
||||
{
|
||||
label: '申请状态',
|
||||
value: getPlUreaApplyStatusLabel(row.applyStatus),
|
||||
tagType: getPlUreaApplyStatusTagType(row.applyStatus)
|
||||
},
|
||||
{ label: '车牌号', value: row.licensePlate },
|
||||
{ label: '单位', value: row.unit },
|
||||
{ label: '入库数', value: Number(row.quantity).toFixed(2) },
|
||||
{ label: '申请金额', value: `${Number(row.applyAmount).toFixed(2)} 元` },
|
||||
{ label: '申请人', value: row.applicantName || '—' },
|
||||
{ label: '申请备注', value: row.applyRemark || '—', span: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
if (row.disburseId) {
|
||||
sections.push({
|
||||
title: '发放信息',
|
||||
fields: [
|
||||
{ label: '发放单号', value: row.disburseNo || '—' },
|
||||
{ label: '发放时间', value: row.disburseDate || '—' },
|
||||
{
|
||||
label: '发放金额',
|
||||
value: row.amount != null ? `${Number(row.amount).toFixed(2)} 元` : '—'
|
||||
},
|
||||
{ label: '处理人', value: row.handlerName || '—' },
|
||||
{ label: '发放备注', value: row.disburseRemark || '—', span: 2 }
|
||||
]
|
||||
})
|
||||
}
|
||||
return sections
|
||||
})
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: unknown = (res as { data?: unknown })?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: Record<string, unknown>) => {
|
||||
const name = String(u.label || u.nickName || '')
|
||||
const account = String(u.userNo || '')
|
||||
return {
|
||||
userId: String(u.value ?? u.id ?? ''),
|
||||
nickName: name,
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const vehicles = await vehicleApi.list()
|
||||
vehicleOptions.value = (vehicles.data ?? vehicles) || []
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleUreaDisburseApi.disburseBoard({
|
||||
q: searchForm.q || undefined,
|
||||
applyStatus: searchForm.applyStatus || undefined,
|
||||
applyDateFrom: applyDateRange.value?.[0],
|
||||
applyDateTo: applyDateRange.value?.[1],
|
||||
disburseDateFrom: disburseDateRange.value?.[0],
|
||||
disburseDateTo: disburseDateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.applyStatus = ''
|
||||
applyDateRange.value = []
|
||||
disburseDateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.disburseNo = ''
|
||||
formData.applyId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.handlerId = ''
|
||||
formData.disburseDate = ''
|
||||
formData.amount = null
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
currentApplyRow.value = null
|
||||
}
|
||||
|
||||
async function handleDisburse(row: VehicleUreaDisburseBoardItem) {
|
||||
resetForm()
|
||||
dialogMode.value = 'disburse'
|
||||
dialogTitle.value = '发放登记'
|
||||
currentApplyRow.value = row
|
||||
formData.applyId = row.applyId
|
||||
formData.licensePlate = row.licensePlate
|
||||
formData.amount = Number(row.applyAmount)
|
||||
formData.disburseDate = new Date().toISOString().slice(0, 10)
|
||||
formData.handlerId = currentUserId.value
|
||||
await loadUserOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSupplement() {
|
||||
resetForm()
|
||||
dialogMode.value = 'supplement'
|
||||
dialogTitle.value = '无申请补录'
|
||||
formData.disburseDate = new Date().toISOString().slice(0, 10)
|
||||
formData.handlerId = currentUserId.value
|
||||
await Promise.all([loadVehicleOptions(), loadUserOptions()])
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleUreaDisburseBoardItem) {
|
||||
if (!row.disburseId) return
|
||||
resetForm()
|
||||
dialogMode.value = 'edit'
|
||||
dialogTitle.value = '编辑发放登记'
|
||||
currentApplyRow.value = row
|
||||
await loadUserOptions()
|
||||
const res = await vehicleUreaDisburseApi.get(row.disburseId)
|
||||
const detail = res.data ?? res
|
||||
Object.assign(formData, {
|
||||
id: detail.id,
|
||||
disburseNo: detail.disburseNo,
|
||||
applyId: detail.applyId || row.applyId,
|
||||
licensePlate: detail.licensePlate || row.licensePlate,
|
||||
handlerId: detail.handlerId,
|
||||
disburseDate: detail.disburseDate,
|
||||
amount: Number(detail.amount),
|
||||
attachmentUrls: detail.attachmentUrls || '',
|
||||
remark: detail.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleView(row: VehicleUreaDisburseBoardItem) {
|
||||
viewRow.value = row
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applyId: formData.applyId || undefined,
|
||||
licensePlate: dialogMode.value === 'supplement' ? formData.licensePlate : undefined,
|
||||
handlerId: formData.handlerId,
|
||||
disburseDate: formData.disburseDate,
|
||||
amount: formData.amount,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (dialogMode.value === 'edit') {
|
||||
await vehicleUreaDisburseApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleUreaDisburseApi.create(payload)
|
||||
ElMessage.success('发放登记成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleUreaDisburseBoardItem) {
|
||||
if (!row.disburseId) return
|
||||
await ElMessageBox.confirm('确定撤销该发放登记吗?关联申请将回退为待发放。', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.disburseId] = true
|
||||
try {
|
||||
await vehicleUreaDisburseApi.delete(row.disburseId)
|
||||
ElMessage.success('已撤销发放')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.disburseId] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.apply-summary-block {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--primary-bg);
|
||||
}
|
||||
|
||||
.apply-summary-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.apply-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
min-width: 72px;
|
||||
margin-right: 6px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -126,7 +126,7 @@
|
||||
<el-button link type="primary" @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click.stop="handleDelete(row)">
|
||||
<el-button link type="danger" :disabled="!canDeleteByCreator(row)" :title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -167,7 +167,7 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.vehicleType === 'carrier'" label="承运商" prop="carrierId">
|
||||
<el-form-item label="承运商" prop="carrierId">
|
||||
<el-select v-model="formData.carrierId" placeholder="请选择承运商" clearable filterable>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
@@ -259,10 +259,12 @@
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 车辆证书(卡片) -->
|
||||
<PlCertFormList
|
||||
<!-- 车辆证书(主车/挂车 Tab) -->
|
||||
<PlVehicleCertPartTabs
|
||||
ref="certFormListRef"
|
||||
v-model="certList"
|
||||
:license-plate="formData.licensePlate"
|
||||
:trailer-no="formData.trailerNo"
|
||||
title="车辆证书"
|
||||
upload-folder="pl/vehicle-cert"
|
||||
:cert-name-options="certNameOptions"
|
||||
@@ -294,6 +296,8 @@
|
||||
<template #vehicleDetailTabs>
|
||||
<PlVehicleCertPersonnelTabs
|
||||
:cert-list="viewDetail?.certList || []"
|
||||
:license-plate="viewDetail?.licensePlate"
|
||||
:trailer-no="viewDetail?.trailerNo"
|
||||
:personnel-list="viewPersonnelList"
|
||||
/>
|
||||
</template>
|
||||
@@ -314,7 +318,7 @@ import {
|
||||
type Carrier,
|
||||
type Personnel
|
||||
} from '@/api/pl'
|
||||
import PlCertFormList from '@/components/business/pl-cert-form-list/index.vue'
|
||||
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
|
||||
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import SystemViewDialog, {
|
||||
@@ -322,9 +326,15 @@ import SystemViewDialog, {
|
||||
} from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import {
|
||||
PL_VEHICLE_CERT_NAME_OPTIONS,
|
||||
splitVehicleCertsByPart,
|
||||
validateVehicleRequiredCerts
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
type CertFormListRef = InstanceType<typeof PlVehicleCertPartTabs>
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
@@ -346,14 +356,7 @@ const viewLoading = ref(false)
|
||||
const viewDetail = ref<VehicleDetail | null>(null)
|
||||
const viewPersonnelList = ref<VehiclePersonnelViewRow[]>([])
|
||||
|
||||
const certNameOptions = [
|
||||
'行驶证',
|
||||
'道路运输证',
|
||||
'驾驶证',
|
||||
'押运员证',
|
||||
'危化品从业资格证',
|
||||
'保险单'
|
||||
]
|
||||
const certNameOptions = PL_VEHICLE_CERT_NAME_OPTIONS
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
@@ -597,13 +600,17 @@ function validateSubTables(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const cert of certList.value) {
|
||||
if (!cert.certName?.trim()) {
|
||||
ElMessage.warning('证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
const hasTrailer = Boolean(formData.trailerNo?.trim())
|
||||
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList.value)
|
||||
return validateVehicleRequiredCerts({
|
||||
headCerts,
|
||||
trailerCerts,
|
||||
hasTrailer,
|
||||
licensePlate: formData.licensePlate,
|
||||
trailerNo: formData.trailerNo,
|
||||
onError: message => ElMessage.warning(message)
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
@@ -720,6 +727,7 @@ async function handleEdit(row: Vehicle) {
|
||||
certList.value = (detail.certList || []).map(c => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate,
|
||||
@@ -769,6 +777,7 @@ async function handleSubmit() {
|
||||
})),
|
||||
certList: certList.value.map(c => ({
|
||||
certId: c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName.trim(),
|
||||
certNo: c.certNo || undefined,
|
||||
expireDate: c.expireDate || undefined,
|
||||
@@ -796,6 +805,10 @@ function handleDialogClose() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Vehicle) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该车辆吗?', '提示', { type: 'warning' })
|
||||
await vehicleApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -256,6 +256,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -702,7 +703,12 @@ const dictStore = useDictStore()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Dept.DeptItem) {
|
||||
async function handleDelete(row: Api.Dept.DeptItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
try {
|
||||
const childCount = row.children?.length || 0
|
||||
const confirmText = childCount
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
@@ -118,7 +119,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -346,7 +347,14 @@
|
||||
<el-button class="btn-op-item primary" @click="handleEditDictData(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDeleteDictData(row)"> 删除 </el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDeleteDictData(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -449,6 +457,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -635,7 +644,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Dict.DictTypeItem) {
|
||||
async function handleDelete(row: Api.Dict.DictTypeItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
await ElMessageBox.confirm(`确定删除字典类型「${row.dictName}」吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
@@ -644,7 +658,16 @@
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map((r) => r.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请选择要删除的字典类型')
|
||||
return
|
||||
@@ -767,6 +790,10 @@
|
||||
}
|
||||
|
||||
async function handleDeleteDictData(row: Api.Dict.DictDataItem) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除字典数据「${row.dictLabel}」吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
@@ -123,7 +123,12 @@
|
||||
<el-button type="success" link size="small" @click="handleSubmit(row.id)"
|
||||
>提交审批</el-button
|
||||
>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row.id)"
|
||||
<el-button
|
||||
v-if="canDeleteByCreator(row)"
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
@@ -237,6 +242,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Close, Check, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
fetchLeaveList,
|
||||
fetchCreateLeave,
|
||||
@@ -365,10 +371,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
async function handleDelete(row: { id: string }) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除此请假申请?', '提示', { type: 'warning' })
|
||||
await fetchDeleteLeave(id)
|
||||
await fetchDeleteLeave(row.id)
|
||||
loadData()
|
||||
} catch {
|
||||
// cancelled or error
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -267,6 +267,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -536,7 +537,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Menu.MenuItem) {
|
||||
async function handleDelete(row: Api.Menu.MenuItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
try {
|
||||
const childCount = row.children?.length || 0
|
||||
const confirmText = childCount
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
<!-- 操作日志 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<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.title" placeholder="模块名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-select v-model="searchForm.businessType" placeholder="全部" clearable>
|
||||
<el-option
|
||||
v-for="opt in OPER_LOG_BUSINESS_TYPE_OPTIONS"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人员">
|
||||
<el-input v-model="searchForm.operName" placeholder="姓名或账号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||||
<el-option label="成功" :value="0" />
|
||||
<el-option label="失败" :value="1" />
|
||||
</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" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @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" stripe v-loading="loading" row-key="id">
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="title" label="操作模块" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="操作类型" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getOperLogBusinessTypeTagType(row.businessType)" size="small">
|
||||
{{ getOperLogBusinessTypeLabel(row.businessType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="operSummary"
|
||||
label="操作摘要"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="operName" label="操作人员" min-width="120" align="center" />
|
||||
<el-table-column prop="operTime" label="操作时间" min-width="170" align="center" />
|
||||
<el-table-column label="状态" min-width="90" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 0 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 0 ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="costTime" label="耗时(ms)" min-width="100" align="center" />
|
||||
<el-table-column
|
||||
label="操作"
|
||||
fixed="right"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="openDetail(row)">
|
||||
<el-icon><View /></el-icon>详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="fetchList"
|
||||
@current-change="fetchList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="操作日志详情"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout operlog-detail-dialog"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-if="detailRow" class="operlog-detail-body">
|
||||
<div class="detail-kv-grid">
|
||||
<div class="detail-kv-item"><span class="label">操作模块</span><span>{{ detailRow.title }}</span></div>
|
||||
<div class="detail-kv-item">
|
||||
<span class="label">操作类型</span>
|
||||
<el-tag :type="getOperLogBusinessTypeTagType(detailRow.businessType)" size="small">
|
||||
{{ getOperLogBusinessTypeLabel(detailRow.businessType) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-kv-item"><span class="label">操作人员</span><span>{{ detailRow.operName || '—' }}</span></div>
|
||||
<div class="detail-kv-item"><span class="label">操作时间</span><span>{{ detailRow.operTime || '—' }}</span></div>
|
||||
<div class="detail-kv-item full"><span class="label">操作摘要</span><span>{{ detailRow.operSummary || '—' }}</span></div>
|
||||
<div class="detail-kv-item full"><span class="label">请求地址</span><span>{{ detailRow.operUrl || '—' }}</span></div>
|
||||
<div class="detail-kv-item"><span class="label">操作 IP</span><span>{{ detailRow.operIp || '—' }}</span></div>
|
||||
<div class="detail-kv-item"><span class="label">耗时</span><span>{{ detailRow.costTime ?? '—' }} ms</span></div>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<div class="detail-block-title">请求参数</div>
|
||||
<pre class="detail-pre">{{ detailRow.operParam || '无' }}</pre>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<div class="detail-block-title">返回参数</div>
|
||||
<pre class="detail-pre">{{ detailRow.jsonResult || '无' }}</pre>
|
||||
</div>
|
||||
<div v-if="detailRow.status === 1" class="detail-block">
|
||||
<div class="detail-block-title">错误信息</div>
|
||||
<pre class="detail-pre error">{{ detailRow.errorMsg || '—' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
||||
import { list as listOperLog, detail as getOperLogDetail } from '@/api/monitor/operlog'
|
||||
import type { OperLogVO, OperLogQuery } from '@/api/monitor/operlog/types'
|
||||
import {
|
||||
OPER_LOG_BUSINESS_TYPE_OPTIONS,
|
||||
getOperLogBusinessTypeLabel,
|
||||
getOperLogBusinessTypeTagType
|
||||
} from '@/api/monitor/operlog/types'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<OperLogVO[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const searchForm = reactive({
|
||||
title: '',
|
||||
businessType: undefined as number | undefined,
|
||||
operName: '',
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailRow = ref<OperLogVO | null>(null)
|
||||
|
||||
function buildQuery(): OperLogQuery {
|
||||
return {
|
||||
pageNum: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
title: searchForm.title || undefined,
|
||||
businessType: searchForm.businessType,
|
||||
operName: searchForm.operName || undefined,
|
||||
status: searchForm.status
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listOperLog(buildQuery())
|
||||
const payload = res as { rows?: OperLogVO[]; list?: OperLogVO[]; total?: number }
|
||||
tableData.value = payload?.rows || payload?.list || []
|
||||
pagination.total = payload?.total ?? tableData.value.length
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.title = ''
|
||||
searchForm.businessType = undefined
|
||||
searchForm.operName = ''
|
||||
searchForm.status = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
async function openDetail(row: OperLogVO) {
|
||||
try {
|
||||
const res = await getOperLogDetail({ id: String(row.id) })
|
||||
detailRow.value = (res as OperLogVO) || row
|
||||
} catch {
|
||||
detailRow.value = row
|
||||
}
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-kv-item {
|
||||
font-size: 14px;
|
||||
color: var(--text, #1e293b);
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #64748b);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-block {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.detail-block-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-pre {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
|
||||
&.error {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -40,7 +40,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
@@ -126,7 +127,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -317,6 +318,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -708,7 +710,12 @@ const dictStore = useDictStore()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Post.PostItem) {
|
||||
async function handleDelete(row: Api.Post.PostItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
await ElMessageBox.confirm(`确定删除职位「${row.postName}」吗?`, '提示', { type: 'warning' })
|
||||
await fetchDeletePost(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
@@ -716,7 +723,16 @@ const dictStore = useDictStore()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map((r) => r.id)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个职位吗?`, '提示', {
|
||||
type: 'warning'
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
@@ -132,7 +133,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -422,6 +423,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -753,14 +755,28 @@ const dictStore = useDictStore()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Role.RoleItem) {
|
||||
async function handleDelete(row: Api.Role.RoleItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
await ElMessageBox.confirm(`确定删除角色「${row.roleName}」吗?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteRole(row.id)
|
||||
await loadData()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map((r) => r.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请至少选择一项')
|
||||
return
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
@@ -155,7 +156,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -299,6 +300,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check, View, Edit, Delete, More, Location, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
@@ -648,7 +650,12 @@ const dictStore = useDictStore()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Site.SiteItem) {
|
||||
async function handleDelete(row: Api.Site.SiteItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
await ElMessageBox.confirm(`确定删除站点「${row.siteName}」吗?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteSite(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
@@ -656,7 +663,16 @@ const dictStore = useDictStore()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map((r) => r.id)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个站点吗?`, '提示', {
|
||||
type: 'warning'
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="deletableSelection.length === 0"
|
||||
:disabled="deletableSelection.length === 0 || !allRowsDeletableByCreator(deletableSelection)"
|
||||
:title="allRowsDeletableByCreator(deletableSelection) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
@@ -141,7 +142,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided :disabled="isDefaultTenantRow(row)">
|
||||
<el-dropdown-item command="delete" divided :disabled="isDefaultTenantRow(row) || !canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -229,6 +230,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -424,6 +426,10 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Tenant.TenantItem) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (isDefaultTenantRow(row)) {
|
||||
ElMessage.warning('默认租户不可删除')
|
||||
return
|
||||
@@ -434,7 +440,16 @@
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = deletableSelection.value.map((r) => r.id)
|
||||
const rows = filterDeletableByCreator(deletableSelection.value)
|
||||
if (!rows.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (rows.length < deletableSelection.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = rows.map((r) => r.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('所选中含默认租户或无可删项,请重新选择')
|
||||
return
|
||||
|
||||
@@ -93,7 +93,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -294,6 +295,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -569,7 +571,16 @@ const dictStore = useDictStore()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const rows = selectedRows.value
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const rows = deletable
|
||||
if (!rows.length) {
|
||||
ElMessage.warning('请至少选择一项')
|
||||
return
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -129,7 +130,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -208,6 +209,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
Plus,
|
||||
Close,
|
||||
Check,
|
||||
@@ -358,14 +360,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Workflow.ProcessDefItem) {
|
||||
async function handleDelete(row: Api.Workflow.ProcessDefItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
await ElMessageBox.confirm('确定要删除该流程吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteProcessDef(row.id)
|
||||
await loadData()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map((r) => r.id)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个流程吗?`, '提示', {
|
||||
type: 'warning'
|
||||
|
||||
Reference in new issue
Block a user