This commit is contained in:
zlwu committed 2026-06-29 11:15:39 +08:00
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
+24 -1
View File
@@ -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'
}
+370
View File
@@ -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,
+42
View File
@@ -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',
+11
View File
@@ -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',
+65
View File
@@ -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))
}
+114
View File
@@ -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'
}
+110
View File
@@ -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: '确定删除',
+12 -1
View File
@@ -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)
}
+13 -1
View File
@@ -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'
+18 -42
View File
@@ -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 = [];
+1 -19
View File
@@ -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
+17 -1
View File
@@ -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: '取消',
+12 -1
View File
@@ -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 {
+7 -1
View File
@@ -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'
+11
View File
@@ -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);
+7 -2
View File
@@ -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)
+13 -3
View File
@@ -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 {
+227 -13
View File
@@ -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
)
+6 -1
View File
@@ -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 {
+6 -1
View File
@@ -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 {
+22 -17
View File
@@ -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'
})
+95 -28
View File
@@ -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>
+34 -21
View File
@@ -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('删除成功')
+8 -2
View File
@@ -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
+32 -5
View File
@@ -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'
})
+13 -3
View File
@@ -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
+8 -2
View File
@@ -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>
+20 -4
View File
@@ -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'
+20 -4
View File
@@ -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
+20 -4
View File
@@ -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'
+18 -3
View File
@@ -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
+13 -2
View File
@@ -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'