提交
This commit is contained in:
1 parent
493f8d6756
commit
d84f4b944c
406 files changed
+58954
-10376
No files matched your search
@@ -224,7 +224,7 @@
|
||||
<el-icon><Upload /></el-icon>提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'draft' || row.status === 'rejected'"
|
||||
v-if="(row.status === 'draft' || row.status === 'rejected') && canDeleteByCreator(row)"
|
||||
class="btn-op-item"
|
||||
:loading="loadingMap[`del_${row.id}`]"
|
||||
@click="handleDelete(row)"
|
||||
@@ -433,6 +433,7 @@ import {
|
||||
ArrowDownBold
|
||||
} from '@element-plus/icons-vue'
|
||||
import { advanceApi, paymentApi } from '@/api/bx'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
statusLabel,
|
||||
statusTagType,
|
||||
@@ -758,6 +759,10 @@ async function handleSubmit(row: any) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该申请单吗?删除后不可恢复。', '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
|
||||
@@ -124,7 +124,13 @@
|
||||
>
|
||||
<el-icon><Plus /></el-icon>新增子项
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" :loading="loadingMap.rowDelete" @click="handleDelete(row)">
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -325,6 +331,7 @@ import {
|
||||
SortDown
|
||||
} from '@element-plus/icons-vue'
|
||||
import { expenseItemApi } from '@/api/bx'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { statusLabel, statusTagType } from '@/utils/bx/format'
|
||||
import { suggestExpenseItemCode } from '@/utils/bx/expenseItemCode'
|
||||
|
||||
@@ -512,6 +519,10 @@ async function handleAddChildSubmit() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除费用项目「${row.itemName || row.itemCode}」吗?`,
|
||||
'删除确认',
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<el-icon><Upload /></el-icon>提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'draft' || row.status === 'rejected'"
|
||||
v-if="(row.status === 'draft' || row.status === 'rejected') && canDeleteByCreator(row)"
|
||||
class="btn-op-item"
|
||||
:loading="loadingMap[`del_${row.id}`]"
|
||||
@click="handleDelete(row)"
|
||||
@@ -474,6 +474,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { reimburseApi, attachmentApi } from '@/api/bx'
|
||||
import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { useExpenseItemTree, type ExpenseItem } from '@/hooks/bx/useExpenseItemTree'
|
||||
import {
|
||||
statusLabel,
|
||||
@@ -911,6 +912,10 @@ async function handleSubmit(row: any) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该报销单吗?删除后不可恢复。', '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
|
||||
@@ -64,7 +64,13 @@
|
||||
<el-button class="btn-op-item" @click="openEditDialog(row)">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" :loading="loadingMap.rowDelete" @click="deleteNode(row)">
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="deleteNode(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="moveUp(row)">
|
||||
@@ -180,6 +186,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import {
|
||||
@@ -365,6 +372,10 @@ async function submitEditNode() {
|
||||
}
|
||||
|
||||
async function deleteNode(node: Record<string, any>) {
|
||||
if (!canDeleteByCreator(node)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定要删除节点「${node.nodeName}」吗?`, '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -64,7 +64,13 @@
|
||||
<el-button class="btn-op-item" @click="openEditDialog(row)">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" :loading="loadingMap.rowDelete" @click="deleteNode(row)">
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="deleteNode(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="moveUp(row)">
|
||||
@@ -180,6 +186,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import {
|
||||
@@ -365,6 +372,10 @@ async function submitEditNode() {
|
||||
}
|
||||
|
||||
async function deleteNode(node: Record<string, any>) {
|
||||
if (!canDeleteByCreator(node)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定要删除节点「${node.nodeName}」吗?`, '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
v-if="hasCluePerm('remove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="clueInfo.isConverted === 1"
|
||||
:disabled="clueInfo.isConverted === 1 || !canDeleteByCreator(clueInfo)"
|
||||
:title="canDeleteByCreator(clueInfo) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
@@ -89,7 +90,7 @@
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasCluePerm('follow') && canManageFollow(item)"
|
||||
v-if="hasCluePerm('follow') && canDeleteByCreator(item)"
|
||||
class="follow-actions"
|
||||
>
|
||||
<el-button link type="primary" size="small" @click="handleEditFollow(item)">
|
||||
@@ -148,6 +149,7 @@
|
||||
import type { ClueVO, ClueFollowVO, ClueFollowQuery } from '@/api/crm/clue/types'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useCluePerm } from '@/hooks/business/useCluePerm'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import ClueForm from './form.vue'
|
||||
import FollowForm from './followForm.vue'
|
||||
@@ -336,11 +338,6 @@
|
||||
clueFormRef.value?.open(clueId.value)
|
||||
}
|
||||
|
||||
const canManageFollow = (item: ClueFollowVO) => {
|
||||
if (!item.createBy) return true
|
||||
return String(item.createBy) === String(currentUserId.value)
|
||||
}
|
||||
|
||||
const handleAddFollow = () => {
|
||||
followFormRef.value?.open(clueId.value)
|
||||
}
|
||||
|
||||
@@ -406,7 +406,8 @@
|
||||
<el-button
|
||||
v-if="hasCluePerm('remove')"
|
||||
class="btn-op-item danger"
|
||||
:disabled="row.isConverted === 1"
|
||||
:disabled="row.isConverted === 1 || !canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
@@ -549,6 +550,7 @@
|
||||
import type { UploadFile } from 'element-plus'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useCluePerm } from '@/hooks/business/useCluePerm'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
listClue,
|
||||
listMyClue,
|
||||
@@ -823,6 +825,16 @@
|
||||
ElMessage.warning('请至少选择一条数据进行删除')
|
||||
return
|
||||
}
|
||||
const targets = row ? [row] : selectedRows.value
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('是否确认删除选中的线索?已转化的线索不允许删除。', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
<el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('changeRemove')" :span="1.5">
|
||||
<el-button type="danger" icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
<el-button type="danger" icon="Delete" :disabled="multiple || !allRowsDeletableByCreator(changeList.filter((item) => ids.includes(item.id)))" :title="allRowsDeletableByCreator(changeList.filter((item) => ids.includes(item.id))) ? '' : CREATOR_DELETE_TIP" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('changeExport')" :span="1.5">
|
||||
<el-button type="success" icon="Download" @click="handleExport">导出</el-button>
|
||||
@@ -135,7 +135,7 @@
|
||||
icon="RefreshLeft"
|
||||
>撤回</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="hasContractPerm('changeRemove') && scope.row.approvalStatus === 'pending'"
|
||||
v-if="hasContractPerm('changeRemove') && scope.row.approvalStatus === 'pending' && canDeleteByCreator(scope.row)"
|
||||
command="delete"
|
||||
icon="Delete"
|
||||
divided
|
||||
@@ -238,6 +238,7 @@ import { useDictStore } from '@/store/modules/dict'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
const router = useRouter()
|
||||
const dictStore = useDictStore()
|
||||
@@ -455,6 +456,16 @@ const submitForm = () => {
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: ChangeVO) => {
|
||||
const targets = row ? [row] : changeList.value.filter((item) => ids.value.includes(item.id))
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const idList = row?.id ? [row.id] : ids.value;
|
||||
await $modal.confirm('是否确认删除选中的变更记录?')
|
||||
await delChange(idList)
|
||||
|
||||
@@ -37,7 +37,11 @@
|
||||
>
|
||||
<el-icon><Edit /></el-icon>修改
|
||||
</el-button>
|
||||
<el-button v-if="hasContractPerm('remove')" class="btn-ghost" @click="handleDetailDelete">
|
||||
<el-button
|
||||
v-if="hasContractPerm('remove') && canDeleteByCreator(detailContract)"
|
||||
class="btn-ghost"
|
||||
@click="handleDetailDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
@@ -161,19 +165,37 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" prop="quantity" width="110" align="right" />
|
||||
<el-table-column label="含税单价(元)" prop="price" width="130" align="right">
|
||||
<el-table-column
|
||||
v-if="!hideProductPrice"
|
||||
label="含税单价(元)"
|
||||
prop="price"
|
||||
width="130"
|
||||
align="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-else class="money-display">¥{{ parseFloat(scope.row.price || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="含税总价(元)" prop="subtotal" width="130" align="right">
|
||||
<el-table-column
|
||||
v-if="!hideProductPrice"
|
||||
label="含税总价(元)"
|
||||
prop="subtotal"
|
||||
width="130"
|
||||
align="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-else class="money-display">¥{{ parseFloat(scope.row.subtotal || 0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="税费(元)" prop="taxAmount" width="120" align="right">
|
||||
<el-table-column
|
||||
v-if="!hideProductPrice"
|
||||
label="税费(元)"
|
||||
prop="taxAmount"
|
||||
width="120"
|
||||
align="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span v-if="detailContract.contractType === 'framework'" class="text-muted">随行就市</span>
|
||||
<span v-else class="money-display">¥{{ parseFloat(scope.row.taxAmount || 0).toFixed(2) }}</span>
|
||||
@@ -198,7 +220,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="detailContract.contractType === 'framework'">
|
||||
<template v-if="detailContract.contractType === 'framework' && !hideProductPrice">
|
||||
<el-divider content-position="left">框架合同调价明细</el-divider>
|
||||
<p class="text-muted" style="font-size: 13px; margin-bottom: 12px">按生效区间记录含税单价及调价函;后续结算可据此核对。</p>
|
||||
<div
|
||||
@@ -247,6 +269,7 @@
|
||||
</template>
|
||||
|
||||
<!-- 合计金额 -->
|
||||
<template v-if="!hideProductPrice">
|
||||
<el-divider />
|
||||
<div class="total-section">
|
||||
<el-row :gutter="20">
|
||||
@@ -320,6 +343,7 @@
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</div>
|
||||
<el-empty v-else description="加载中..." />
|
||||
</el-tab-pane>
|
||||
@@ -573,7 +597,7 @@
|
||||
<el-descriptions-item label="完成说明" :span="2">
|
||||
<div class="remark-content">{{ detailContract?.remark || '-' }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行情况汇总" :span="2">
|
||||
<el-descriptions-item v-if="!hideProductPrice" label="执行情况汇总" :span="2">
|
||||
<el-row :gutter="20" class="mt-2">
|
||||
<el-col :span="8">
|
||||
<el-statistic title="合同金额(元)" :value="detailContract?.amount || 0" :precision="2" class="money-statistic">
|
||||
@@ -794,6 +818,7 @@ import DictTag from '@/components/DictTag/index.vue';
|
||||
import { $modal } from '@/utils/modal';
|
||||
import { parseTime } from '@/utils/format';
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm';
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete';
|
||||
import ApprovalDialog from './ApprovalDialog.vue';
|
||||
import ExecuteDialog from './ExecuteDialog.vue';
|
||||
import InvalidDialog from './InvalidDialog.vue';
|
||||
@@ -824,6 +849,15 @@ import {
|
||||
const dictStore = useDictStore();
|
||||
const router = useRouter();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
hideProductPrice?: boolean
|
||||
}>(),
|
||||
{
|
||||
hideProductPrice: false
|
||||
}
|
||||
)
|
||||
|
||||
const visible = ref(false);
|
||||
const detailLoading = ref(false);
|
||||
const detailActiveTab = ref('contract');
|
||||
@@ -1330,7 +1364,9 @@ const detailBasicViewFields = computed<ViewField[]>(() => {
|
||||
long: false
|
||||
},
|
||||
{ label: '负责人', value: c.ownerUserName },
|
||||
{ label: '合同金额(元)', value: `¥${formatDetailAmount(c.amount)}`, money: true },
|
||||
...(props.hideProductPrice
|
||||
? []
|
||||
: [{ label: '合同金额(元)', value: `¥${formatDetailAmount(c.amount)}`, money: true }]),
|
||||
{ label: '交易主体', value: c.supplierName },
|
||||
detailTagField('结算方式', 'crm_settlement_method', c.settlementMethod)
|
||||
];
|
||||
|
||||
@@ -40,15 +40,6 @@
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联商机" prop="opportunityId">
|
||||
<el-input v-model="form.opportunityName" placeholder="请选择商机(可选)" readonly clearable>
|
||||
<template #append>
|
||||
<el-button text @click="handleSelectOpportunity">
|
||||
<el-icon><Search /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="关联报价单" prop="quoteId">
|
||||
<el-input v-model="form.quoteNo" placeholder="请选择报价单(可选)" readonly clearable>
|
||||
<template #append>
|
||||
@@ -501,14 +492,6 @@
|
||||
@confirm="handleContactConfirm"
|
||||
/>
|
||||
|
||||
<!-- 商机选择器 -->
|
||||
<OpportunitySelector
|
||||
:visible="opportunitySelectorVisible"
|
||||
@update:visible="opportunitySelectorVisible = $event"
|
||||
:customerId="form.customerId"
|
||||
@confirm="handleOpportunityConfirm"
|
||||
/>
|
||||
|
||||
<!-- 报价单选择器 -->
|
||||
<QuoteSelector
|
||||
:visible="quoteSelectorVisible"
|
||||
@@ -563,7 +546,6 @@ import { getQuote } from '@/api/crm/quote'
|
||||
import ProductSelector from './ProductSelector.vue'
|
||||
import CustomerSelector from '@/components/CustomerSelector.vue'
|
||||
import ContactSelector from '@/components/ContactSelector.vue'
|
||||
import OpportunitySelector from '@/components/OpportunitySelector.vue'
|
||||
import QuoteSelector from '@/components/QuoteSelector.vue'
|
||||
import ApprovalDialog from './ApprovalDialog.vue'
|
||||
import { getContactByCustomerId } from '@/api/crm/customer'
|
||||
|
||||
@@ -28,7 +28,12 @@
|
||||
@click="handleEdit"
|
||||
v-if="hasContractPerm('edit') && contract?.status === 'draft'"
|
||||
>修改</el-button>
|
||||
<el-button type="danger" icon="Delete" @click="handleDelete" v-if="hasContractPerm('remove')">删除</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('remove') && canDeleteByCreator(contract)"
|
||||
type="danger"
|
||||
icon="Delete"
|
||||
@click="handleDelete"
|
||||
>删除</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
icon="Promotion"
|
||||
@@ -1021,6 +1026,7 @@ import {
|
||||
downloadCrmAttachment
|
||||
} from '@/utils/crm-attachment';
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm';
|
||||
import { canDeleteByCreator } from '@/utils/creator-delete';
|
||||
import html2canvas from 'html2canvas';
|
||||
import { jsPDF } from 'jspdf';
|
||||
|
||||
@@ -1225,47 +1231,17 @@ const getOperLogList = async (reload: boolean = false) => {
|
||||
if (!contract.value?.id) return;
|
||||
logLoading.value = true;
|
||||
try {
|
||||
// 如果需要重新加载或者还没有加载过完整数据,则从服务器获取
|
||||
if (reload || operLogListFull.value.length === 0) {
|
||||
// 获取所有操作日志,不限制模块
|
||||
const query = {
|
||||
pageNum: 1,
|
||||
pageSize: 100 // 获取更多记录用于筛选
|
||||
};
|
||||
const res = await listOperLog(query);
|
||||
const payload = res as { rows?: OperLogVO[]; list?: OperLogVO[] } | OperLogVO[];
|
||||
const rows = Array.isArray(payload) ? payload : payload?.rows || payload?.list || [];
|
||||
|
||||
// 过滤出与当前合同相关的日志
|
||||
const contractIdStr = String(contract.value.id);
|
||||
operLogListFull.value = rows.filter((log: OperLogVO) => {
|
||||
// 检查URL是否包含合同ID
|
||||
const urlMatch = log.operUrl && log.operUrl.includes(`/crm/contract`) && log.operUrl.includes(`/${contractIdStr}`);
|
||||
|
||||
// 检查参数中是否包含合同ID(支持多种格式)
|
||||
const paramMatch =
|
||||
log.operParam &&
|
||||
(log.operParam.includes(`"id":${contractIdStr}`) ||
|
||||
log.operParam.includes(`"id":"${contractIdStr}"`) ||
|
||||
log.operParam.includes(`"contractId":${contractIdStr}`) ||
|
||||
log.operParam.includes(`"contractId":"${contractIdStr}"`) ||
|
||||
log.operParam.includes(`[${contractIdStr}]`) ||
|
||||
log.operParam.includes(`["${contractIdStr}"]`));
|
||||
|
||||
return urlMatch || paramMatch;
|
||||
});
|
||||
|
||||
// 按时间倒序排序
|
||||
operLogListFull.value.sort((a, b) => {
|
||||
return new Date(b.operTime).getTime() - new Date(a.operTime).getTime();
|
||||
});
|
||||
}
|
||||
|
||||
// 从完整列表中进行分页
|
||||
const start = (logQuery.value.pageNum - 1) * logQuery.value.pageSize;
|
||||
const end = start + logQuery.value.pageSize;
|
||||
logTotal.value = operLogListFull.value.length;
|
||||
operLogList.value = operLogListFull.value.slice(start, end);
|
||||
const query = {
|
||||
pageNum: logQuery.value.pageNum,
|
||||
pageSize: logQuery.value.pageSize,
|
||||
businessId: String(contract.value.id),
|
||||
businessTitle: 'CRM合同管理'
|
||||
};
|
||||
const res = await listOperLog(query);
|
||||
const payload = res as { rows?: OperLogVO[]; list?: OperLogVO[]; total?: number };
|
||||
operLogListFull.value = payload?.rows || payload?.list || [];
|
||||
logTotal.value = payload?.total ?? operLogListFull.value.length;
|
||||
operLogList.value = operLogListFull.value;
|
||||
} catch (error) {
|
||||
console.error('获取操作日志失败:', error);
|
||||
operLogList.value = [];
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 第一行:客户名称、关联商机、关联报价单、联系人 -->
|
||||
<!-- 第一行:客户名称、关联报价单、联系人 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="客户名称" prop="customerId" required>
|
||||
@@ -51,15 +51,6 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="关联商机" prop="opportunityId">
|
||||
<el-input v-model="form.opportunityName" placeholder="请选择商机(可选)" readonly clearable>
|
||||
<template #append>
|
||||
<el-button text icon="Search" @click="handleSelectOpportunity" />
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="关联报价单" prop="quoteId">
|
||||
<el-input v-model="form.quoteNo" placeholder="请选择报价单(可选)" readonly clearable>
|
||||
@@ -560,14 +551,6 @@
|
||||
@confirm="handleContactConfirm"
|
||||
/>
|
||||
|
||||
<!-- 商机选择器 -->
|
||||
<OpportunitySelector
|
||||
:visible="opportunitySelectorVisible"
|
||||
@update:visible="opportunitySelectorVisible = $event"
|
||||
:customerId="form.customerId"
|
||||
@confirm="handleOpportunityConfirm"
|
||||
/>
|
||||
|
||||
<!-- 报价单选择器 -->
|
||||
<QuoteSelector
|
||||
:visible="quoteSelectorVisible"
|
||||
@@ -612,7 +595,6 @@ import { getQuote } from '@/api/crm/quote';
|
||||
import ProductSelector from './components/ProductSelector.vue';
|
||||
import CustomerSelector from '@/components/CustomerSelector.vue';
|
||||
import ContactSelector from '@/components/ContactSelector.vue';
|
||||
import OpportunitySelector from '@/components/OpportunitySelector.vue';
|
||||
import QuoteSelector from '@/components/QuoteSelector.vue';
|
||||
import UserSelector from '@/components/UserSelector.vue';
|
||||
import ApprovalDialog from './components/ApprovalDialog.vue';
|
||||
|
||||
@@ -571,6 +571,7 @@ import { $modal } from '@/utils/modal'
|
||||
import { parseTime } from '@/utils/format'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm'
|
||||
import { filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import ApprovalDialog from './components/ApprovalDialog.vue'
|
||||
import ExecuteDialog from './components/ExecuteDialog.vue'
|
||||
import InvalidDialog from './components/InvalidDialog.vue'
|
||||
@@ -1175,6 +1176,15 @@ const handleDelete = async (row?: ContractVO) => {
|
||||
$modal.msgWarning('只能删除草稿状态的合同');
|
||||
return;
|
||||
}
|
||||
const deletable = filterDeletableByCreator(selectedContracts)
|
||||
if (!deletable.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedContracts.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await $modal.confirm('是否确认删除选中的合同?只能删除草稿状态的合同');
|
||||
await delContract(idList);
|
||||
$modal.msgSuccess('删除成功');
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('templateRemove')" :span="1.5">
|
||||
<el-button type="danger" icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
<el-button type="danger" icon="Delete" :disabled="multiple || !allRowsDeletableByCreator(templateList.filter((item) => ids.includes(item.id)))" :title="allRowsDeletableByCreator(templateList.filter((item) => ids.includes(item.id))) ? '' : CREATOR_DELETE_TIP" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('templateExport')" :span="1.5">
|
||||
<el-button type="success" icon="Download" @click="handleExport">导出</el-button>
|
||||
@@ -93,7 +93,7 @@
|
||||
<el-tooltip v-if="hasContractPerm('templateEdit')" content="编辑" placement="top">
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="hasContractPerm('templateRemove')" content="删除" placement="top">
|
||||
<el-tooltip v-if="hasContractPerm('templateRemove') && canDeleteByCreator(scope.row)" content="删除" placement="top">
|
||||
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
@@ -203,6 +203,7 @@ import { useDictStore } from '@/store/modules/dict'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const { loadPerms, hasContractPerm } = useContractPerm()
|
||||
@@ -365,6 +366,16 @@ const submitForm = () => {
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: TemplateVO) => {
|
||||
const targets = row ? [row] : templateList.value.filter((item) => ids.value.includes(item.id))
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const idList = row?.id ? [row.id] : ids.value;
|
||||
await $modal.confirm('是否确认删除选中的合同模板?')
|
||||
await delTemplate(idList)
|
||||
|
||||
@@ -36,7 +36,13 @@
|
||||
<el-button class="btn-main" type="primary" @click="handleEdit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" type="danger" @click="handleDelete">
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(customerInfo)"
|
||||
:title="canDeleteByCreator(customerInfo) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -73,14 +79,11 @@
|
||||
}}</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="
|
||||
item.ownerUserName === userStore.info.realName ||
|
||||
item.createByName === userStore.info.realName ||
|
||||
userStore.info.roles?.includes('admin')
|
||||
"
|
||||
v-if="canDeleteByCreator(item)"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
:title="canDeleteByCreator(item) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDeleteFollow(item)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
@@ -199,7 +202,13 @@
|
||||
<el-button link type="primary" @click="handleEditContact(row)">
|
||||
<el-icon><Edit /></el-icon>修改
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDeleteContact(row)">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDeleteContact(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -487,6 +496,7 @@ import { list as listOperLog } from '@/api/monitor/operlog'
|
||||
import type { OperLogVO } from '@/api/monitor/operlog/types'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import CustomerForm from './CustomerFormDialog.vue'
|
||||
import FollowForm from './FollowFormDialog.vue'
|
||||
import ContactForm from './components/ContactFormDialog.vue'
|
||||
@@ -751,39 +761,16 @@ async function getOperLogList(reload = false) {
|
||||
if (!customerId.value) return
|
||||
logLoading.value = true
|
||||
try {
|
||||
if (reload || !operLogListFull.value.length) {
|
||||
const res = await listOperLog({ pageNum: 1, pageSize: 100 })
|
||||
const payload = res as { rows?: OperLogVO[]; list?: OperLogVO[] } | OperLogVO[]
|
||||
const rows = Array.isArray(payload)
|
||||
? payload
|
||||
: payload?.rows || payload?.list || []
|
||||
const customerIdStr = String(customerId.value)
|
||||
const matchCustomerId = (text?: string) => {
|
||||
if (!text) return false
|
||||
return (
|
||||
text.includes(`"id":${customerIdStr}`) ||
|
||||
text.includes(`"id":"${customerIdStr}"`) ||
|
||||
text.includes(`"customerId":${customerIdStr}`) ||
|
||||
text.includes(`"customerId":"${customerIdStr}"`) ||
|
||||
text.includes(`[${customerIdStr}]`) ||
|
||||
text.includes(`["${customerIdStr}"]`) ||
|
||||
text.includes(`/${customerIdStr}`)
|
||||
)
|
||||
}
|
||||
operLogListFull.value = rows.filter((log: OperLogVO) => {
|
||||
const urlMatch =
|
||||
log.operUrl?.includes('/crm/customer') && matchCustomerId(log.operUrl)
|
||||
const paramMatch = matchCustomerId(log.operParam)
|
||||
const resultMatch = matchCustomerId(log.jsonResult)
|
||||
return urlMatch || paramMatch || resultMatch
|
||||
})
|
||||
operLogListFull.value.sort(
|
||||
(a, b) => new Date(b.operTime || 0).getTime() - new Date(a.operTime || 0).getTime()
|
||||
)
|
||||
}
|
||||
const start = (logQuery.value.pageNum - 1) * logQuery.value.pageSize
|
||||
logTotal.value = operLogListFull.value.length
|
||||
operLogList.value = operLogListFull.value.slice(start, start + logQuery.value.pageSize)
|
||||
const res = await listOperLog({
|
||||
pageNum: logQuery.value.pageNum,
|
||||
pageSize: logQuery.value.pageSize,
|
||||
businessId: String(customerId.value),
|
||||
businessTitle: 'CRM客户管理'
|
||||
})
|
||||
const payload = res as { rows?: OperLogVO[]; list?: OperLogVO[]; total?: number }
|
||||
operLogListFull.value = payload?.rows || payload?.list || []
|
||||
logTotal.value = payload?.total ?? operLogListFull.value.length
|
||||
operLogList.value = operLogListFull.value
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
operLogList.value = []
|
||||
|
||||
@@ -75,7 +75,13 @@
|
||||
<el-button v-if="row.isPrimary !== 1" link type="warning" @click="handleSetPrimary(row)">
|
||||
<el-icon><Star /></el-icon>设为主要
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -90,6 +96,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
fetchContactsByCustomerId,
|
||||
fetchDeleteContact,
|
||||
|
||||
@@ -99,7 +99,13 @@
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -162,6 +168,7 @@ import { ref, watch } from 'vue'
|
||||
import { Plus, Edit, Delete, Star, Close, Check } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/http'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
interface InvoiceInfoVO {
|
||||
id: string | number
|
||||
|
||||
@@ -607,7 +607,12 @@
|
||||
<el-button class="btn-op-item" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item btn-op-danger" @click="handleDelete(row)">
|
||||
<el-button
|
||||
class="btn-op-item btn-op-danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -703,6 +708,7 @@ import {
|
||||
Edit,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
fetchCustomerList,
|
||||
fetchCustomerDetail,
|
||||
@@ -983,6 +989,16 @@ async function handleDelete(row?: any) {
|
||||
ElMessage.warning('请至少选择一条数据进行删除')
|
||||
return
|
||||
}
|
||||
const targets = row ? [row] : selectedRows.value
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('是否确认删除选中的客户数据?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
<el-button
|
||||
class="btn-op-item danger"
|
||||
v-auth="'delete'"
|
||||
v-if="canDeleteByCreator(row)"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -224,6 +225,7 @@ import {
|
||||
fetchExecutePoolRules,
|
||||
} from '@/api/crm/pool'
|
||||
import RuleDialog from './components/RuleDialog.vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
v-if="hasOpportunityPerm('remove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="opportunityInfo.status === 'won'"
|
||||
:disabled="opportunityInfo.status === 'won' || !canDeleteByCreator(opportunityInfo)"
|
||||
:title="canDeleteByCreator(opportunityInfo) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
@@ -79,7 +80,7 @@
|
||||
</el-tag>
|
||||
<span class="follow-creator">{{ item.followByName || item.createByName || '—' }}</span>
|
||||
</div>
|
||||
<div v-if="String(item.followBy) === String(currentUserId)" class="follow-actions">
|
||||
<div v-if="canDeleteByCreator(item)" class="follow-actions">
|
||||
<el-button link type="primary" size="small" @click="handleEditFollow(item)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
@@ -190,6 +191,7 @@ import AttachmentUploadField from '@/components/business/attachment-upload-field
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useOpportunityPerm } from '@/hooks/business/useOpportunityPerm'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
getOpportunity,
|
||||
listFollowByOpportunity,
|
||||
|
||||
@@ -459,6 +459,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useOpportunityPerm } from '@/hooks/business/useOpportunityPerm'
|
||||
import { filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import { listOpportunity, listMyOpportunity, delOpportunity, assignOpportunity, exportOpportunity } from '@/api/crm/opportunity'
|
||||
import type { OpportunityQuery, OpportunityVO } from '@/api/crm/opportunity/types'
|
||||
@@ -777,6 +778,16 @@ const handleDelete = async (row?: OpportunityVO) => {
|
||||
ElMessage.warning('已赢单的商机不允许删除');
|
||||
return;
|
||||
}
|
||||
const targets = row ? [row] : opportunityList.value.filter((item) => ids.value.includes(item.id))
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const idList = row?.id ? [row.id] : ids.value;
|
||||
await ElMessageBox.confirm('是否确认删除选中的商机数据?删除后将无法恢复!', '提示', {
|
||||
type: 'warning'
|
||||
|
||||
@@ -75,7 +75,8 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="!selectedId"
|
||||
:disabled="!selectedId || !canDeleteSelectedCategory"
|
||||
:title="canDeleteSelectedCategory ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -219,7 +220,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { ref, reactive, onMounted, nextTick, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import {
|
||||
@@ -232,6 +233,7 @@ import {
|
||||
RefreshLeft,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
fetchCategoryTree,
|
||||
fetchCreateCategory,
|
||||
@@ -249,6 +251,12 @@ const refreshTable = ref(true)
|
||||
const selectedId = ref<string>()
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
const canDeleteSelectedCategory = computed(() => {
|
||||
if (!selectedId.value) return false
|
||||
const node = findNodeById(categoryList.value, selectedId.value)
|
||||
return canDeleteByCreator(node)
|
||||
})
|
||||
|
||||
// ── 弹窗数据 ──
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
@@ -377,6 +385,11 @@ async function handleDelete() {
|
||||
ElMessage.warning('请选择一条数据进行删除')
|
||||
return
|
||||
}
|
||||
const node = findNodeById(categoryList.value, selectedId.value)
|
||||
if (!canDeleteByCreator(node)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('是否确认删除该产品分类?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
<el-dropdown-item command="view">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -490,6 +490,7 @@ import SystemViewDialog, {
|
||||
import { crmDictField, crmField, crmLongField, crmTagField } from '@/utils/crm/view-fields'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import {
|
||||
fetchCategoryTree,
|
||||
@@ -735,6 +736,16 @@ async function handleDelete(row?: any) {
|
||||
ElMessage.warning('请至少选择一条数据进行删除')
|
||||
return
|
||||
}
|
||||
const targets = row ? [row] : selectedRows.value
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('是否确认删除选中的产品?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
|
||||
@@ -53,9 +53,10 @@
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="quoteInfo.status !== 'invalid'"
|
||||
v-if="quoteInfo.status !== 'invalid' && canDeleteByCreator(quoteInfo)"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:title="canDeleteByCreator(quoteInfo) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
@@ -312,6 +313,7 @@ const CustomerDetailDialog = defineAsyncComponent(
|
||||
)
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useQuotePerm } from '@/hooks/business/useQuotePerm'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { parseTime } from '@/utils/format'
|
||||
import {
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
<el-button v-if="quote?.status === 'draft'" link type="primary" @click="handleSubmit">
|
||||
<el-icon><CircleCheck /></el-icon>提交审批
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete">
|
||||
<el-button
|
||||
v-if="quote?.status === 'draft' && canDeleteByCreator(quote)"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -384,6 +389,7 @@ const dictStore = useDictStore()
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import ListPagination from '@/components/ListPagination/index.vue'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { canDeleteByCreator } from '@/utils/creator-delete'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Back, CircleCheck, Close, Delete, Document, Download, Edit, Printer } from '@element-plus/icons-vue'
|
||||
|
||||
@@ -816,6 +816,7 @@ import html2canvas from 'html2canvas';
|
||||
import jsPDF from 'jspdf';
|
||||
|
||||
import { useQuotePerm } from '@/hooks/business/useQuotePerm'
|
||||
import { filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import QuoteFormDialog from './components/QuoteFormDialog.vue'
|
||||
import QuoteDetailDialog from './components/QuoteDetailDialog.vue'
|
||||
|
||||
@@ -1425,6 +1426,16 @@ const handleDelete = async (row?: QuoteVO) => {
|
||||
$modal.msgWarning('只能删除草稿状态的报价单');
|
||||
return;
|
||||
}
|
||||
const targets = row ? [row] : quoteList.value.filter((item) => ids.value.includes(item.id))
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const idList = row?.id ? [row.id] : ids.value;
|
||||
await $modal.confirm('是否确认删除选中的报价单?只能删除草稿状态的报价单');
|
||||
await delQuote(idList);
|
||||
|
||||
@@ -371,8 +371,8 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item danger"
|
||||
:disabled="!canDeleteRecord(row)"
|
||||
:title="canDeleteRecord(row) ? '' : '仅支持删除当天及之后日期的记录'"
|
||||
:disabled="!canDeleteRecord(row) || !canDeleteByCreator(row)"
|
||||
:title="!canDeleteRecord(row) ? '仅支持删除当天及之后日期的记录' : canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -514,6 +514,7 @@
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
|
||||
import DbInventoryRelatedViewDialog from '@/components/business/db/DbInventoryRelatedViewDialog.vue'
|
||||
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
|
||||
@@ -884,6 +885,10 @@
|
||||
$modal.msgWarning('仅支持删除当天及之后日期的记录,历史数据不可删除')
|
||||
return
|
||||
}
|
||||
if (!canDeleteByCreator(row)) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await $modal.confirm('确定要删除这条记录吗?')
|
||||
await inventoryApi.delete(row.id)
|
||||
|
||||
@@ -287,7 +287,12 @@
|
||||
<el-icon><DocumentAdd /></el-icon>
|
||||
生成出入库单
|
||||
</el-button>
|
||||
<el-button class="btn-op-item danger" @click="handleDelete(row.id)">
|
||||
<el-button
|
||||
class="btn-op-item danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
@@ -459,6 +464,7 @@
|
||||
import DbPlanRelatedViewDialog from '@/components/business/db/DbPlanRelatedViewDialog.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
planApi,
|
||||
type Plan,
|
||||
@@ -769,10 +775,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const handleDelete = async (row: Plan) => {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await $modal.confirm('确定要删除这个计划吗?')
|
||||
await planApi.delete(id)
|
||||
await planApi.delete(row.id)
|
||||
$modal.msgSuccess('删除成功')
|
||||
loadData()
|
||||
} catch {
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
<el-option label="已终止" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核状态">
|
||||
<el-select v-model="searchForm.auditStatus" placeholder="全部" clearable>
|
||||
<el-option label="待审核" value="pending" />
|
||||
<el-option label="已通过" value="approved" />
|
||||
<el-option label="已驳回" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
@@ -73,6 +80,18 @@
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === '1' ? '合作中' : '已终止' }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
:type="getPlCarrierAuditStatusTagType(row.auditStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ getPlCarrierAuditStatusLabel(row.auditStatus) }}
|
||||
</el-tag>
|
||||
<el-tag v-if="row.transportAgreementExpired" type="danger" size="small">
|
||||
协议已过期
|
||||
</el-tag>
|
||||
<el-tag v-else-if="row.qualificationComplete === false" type="warning" size="small">
|
||||
资质不全
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -100,7 +119,16 @@
|
||||
<el-button link type="primary" @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click.stop="handleDelete(row)">
|
||||
<el-button
|
||||
v-if="canApproveCarrier(row)"
|
||||
link
|
||||
type="primary"
|
||||
:loading="auditLoadingMap[row.id]"
|
||||
@click.stop="handleApprove(row)"
|
||||
>
|
||||
<el-icon><Select /></el-icon>审核通过
|
||||
</el-button>
|
||||
<el-button link type="danger" :disabled="!canDeleteByCreator(row)" :title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -113,7 +141,7 @@
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="520px"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout"
|
||||
@close="handleDialogClose"
|
||||
@@ -145,6 +173,52 @@
|
||||
maxlength="512"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">资质附件</el-divider>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="营业执照" prop="businessLicenseUrls">
|
||||
<AttachmentUploadField
|
||||
ref="businessLicenseUploadRef"
|
||||
v-model="formData.businessLicenseUrls"
|
||||
:limit="5"
|
||||
accept="image/*,.pdf"
|
||||
folder="pl/carrier/license"
|
||||
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
|
||||
@update:model-value="handleCertFieldChange('businessLicenseUrls')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="道路经营许可证" prop="roadTransportLicenseUrls">
|
||||
<AttachmentUploadField
|
||||
ref="roadTransportLicenseUploadRef"
|
||||
v-model="formData.roadTransportLicenseUrls"
|
||||
:limit="5"
|
||||
accept="image/*,.pdf"
|
||||
folder="pl/carrier/permit"
|
||||
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
|
||||
@update:model-value="handleCertFieldChange('roadTransportLicenseUrls')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运输协议" prop="transportAgreementUrls" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="transportAgreementUploadRef"
|
||||
v-model="formData.transportAgreementUrls"
|
||||
:limit="5"
|
||||
accept="image/*,.pdf"
|
||||
folder="pl/carrier/agreement"
|
||||
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
|
||||
@update:model-value="handleCertFieldChange('transportAgreementUrls')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="协议有效期" prop="transportAgreementExpireDate">
|
||||
<el-date-picker
|
||||
v-model="formData.transportAgreementExpireDate"
|
||||
type="date"
|
||||
placeholder="请选择协议有效期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
@@ -163,25 +237,36 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Select, View } from '@element-plus/icons-vue'
|
||||
import { carrierApi, type Carrier } from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlCarrierViewDialog from '@/components/business/pl-carrier-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
getPlCarrierAuditStatusLabel,
|
||||
getPlCarrierAuditStatusTagType
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const auditLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const selectedRows = ref<Carrier[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const formRef = ref()
|
||||
const businessLicenseUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const roadTransportLicenseUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const transportAgreementUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
const isEdit = ref(false)
|
||||
const viewVisible = ref(false)
|
||||
const viewCarrierId = ref('')
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
status: ''
|
||||
status: '',
|
||||
auditStatus: ''
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
@@ -190,11 +275,70 @@ const formData = reactive({
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
status: '1',
|
||||
remark: ''
|
||||
remark: '',
|
||||
businessLicenseUrls: '',
|
||||
roadTransportLicenseUrls: '',
|
||||
transportAgreementUrls: '',
|
||||
transportAgreementExpireDate: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
carrierName: [{ required: true, message: '请输入承运商名称', trigger: 'blur' }]
|
||||
const createQualificationRules = {
|
||||
businessLicenseUrls: [
|
||||
{
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请上传营业执照'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
roadTransportLicenseUrls: [
|
||||
{
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请上传道路经营许可证'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
transportAgreementUrls: [
|
||||
{
|
||||
validator: (_rule: unknown, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请上传运输协议'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
transportAgreementExpireDate: [
|
||||
{ required: true, message: '请选择协议有效期', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
const formRules = computed(() => ({
|
||||
carrierName: [{ required: true, message: '请输入承运商名称', trigger: 'blur' }],
|
||||
...(isEdit.value ? {} : createQualificationRules)
|
||||
}))
|
||||
|
||||
function handleCertFieldChange(
|
||||
field: 'businessLicenseUrls' | 'roadTransportLicenseUrls' | 'transportAgreementUrls'
|
||||
) {
|
||||
formRef.value?.validateField(field)
|
||||
}
|
||||
|
||||
function hasAttachmentUploading() {
|
||||
return [businessLicenseUploadRef, roadTransportLicenseUploadRef, transportAgreementUploadRef].some(
|
||||
item => item.value?.hasUploading?.()
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
@@ -202,7 +346,8 @@ async function fetchData() {
|
||||
try {
|
||||
const res = await carrierApi.list({
|
||||
q: searchForm.q || undefined,
|
||||
status: searchForm.status || undefined
|
||||
status: searchForm.status || undefined,
|
||||
auditStatus: searchForm.auditStatus || undefined
|
||||
})
|
||||
carrierList.value = (res.data ?? res) || []
|
||||
const idSet = new Set(carrierList.value.map(item => item.id))
|
||||
@@ -219,10 +364,35 @@ function handleSearch() {
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.status = ''
|
||||
searchForm.auditStatus = ''
|
||||
selectedRows.value = []
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function canApproveCarrier(row: Carrier) {
|
||||
return row.auditStatus !== 'approved' && row.qualificationComplete
|
||||
}
|
||||
|
||||
async function handleApprove(row: Carrier) {
|
||||
if (!canApproveCarrier(row)) {
|
||||
ElMessage.warning('承运商资质不完整,无法审核通过')
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(
|
||||
`确认审核通过承运商「${row.carrierName}」?通过后其车辆可用于运输安排。`,
|
||||
'审核确认',
|
||||
{ type: 'info', confirmButtonText: '审核通过', cancelButtonText: '取消' }
|
||||
)
|
||||
auditLoadingMap[row.id] = true
|
||||
try {
|
||||
await carrierApi.approve(row.id)
|
||||
ElMessage.success('审核通过')
|
||||
fetchData()
|
||||
} finally {
|
||||
auditLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
function isSelected(row: Carrier) {
|
||||
return selectedRows.value.some(item => item.id === row.id)
|
||||
}
|
||||
@@ -246,7 +416,11 @@ function handleAdd() {
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
status: '1',
|
||||
remark: ''
|
||||
remark: '',
|
||||
businessLicenseUrls: '',
|
||||
roadTransportLicenseUrls: '',
|
||||
transportAgreementUrls: '',
|
||||
transportAgreementExpireDate: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -260,7 +434,11 @@ function handleEdit(row: Carrier) {
|
||||
contactName: row.contactName,
|
||||
contactPhone: row.contactPhone,
|
||||
status: row.status,
|
||||
remark: row.remark
|
||||
remark: row.remark,
|
||||
businessLicenseUrls: row.businessLicenseUrls || '',
|
||||
roadTransportLicenseUrls: row.roadTransportLicenseUrls || '',
|
||||
transportAgreementUrls: row.transportAgreementUrls || '',
|
||||
transportAgreementExpireDate: row.transportAgreementExpireDate || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -273,13 +451,45 @@ function handleView(row: Carrier) {
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
if (hasAttachmentUploading()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
carrierName: formData.carrierName,
|
||||
contactName: formData.contactName,
|
||||
contactPhone: formData.contactPhone,
|
||||
status: formData.status,
|
||||
remark: formData.remark,
|
||||
...(isEdit.value
|
||||
? {
|
||||
...(formData.businessLicenseUrls
|
||||
? { businessLicenseUrls: formData.businessLicenseUrls }
|
||||
: {}),
|
||||
...(formData.roadTransportLicenseUrls
|
||||
? { roadTransportLicenseUrls: formData.roadTransportLicenseUrls }
|
||||
: {}),
|
||||
...(formData.transportAgreementUrls
|
||||
? { transportAgreementUrls: formData.transportAgreementUrls }
|
||||
: {}),
|
||||
...(formData.transportAgreementExpireDate
|
||||
? { transportAgreementExpireDate: formData.transportAgreementExpireDate }
|
||||
: {})
|
||||
}
|
||||
: {
|
||||
businessLicenseUrls: formData.businessLicenseUrls,
|
||||
roadTransportLicenseUrls: formData.roadTransportLicenseUrls,
|
||||
transportAgreementUrls: formData.transportAgreementUrls,
|
||||
transportAgreementExpireDate: formData.transportAgreementExpireDate
|
||||
})
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await carrierApi.update(formData.id, formData)
|
||||
await carrierApi.update(formData.id, payload)
|
||||
} else {
|
||||
await carrierApi.create(formData)
|
||||
await carrierApi.create(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
dialogVisible.value = false
|
||||
@@ -294,6 +504,10 @@ function handleDialogClose() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Carrier) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该承运商吗?', '提示', { type: 'warning' })
|
||||
await carrierApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
@@ -301,7 +515,7 @@ async function handleDelete(row: Carrier) {
|
||||
}
|
||||
|
||||
useDbInstantSearch(
|
||||
() => ({ q: searchForm.q, status: searchForm.status }),
|
||||
() => ({ q: searchForm.q, status: searchForm.status, auditStatus: searchForm.auditStatus }),
|
||||
handleSearch
|
||||
)
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">
|
||||
<el-button link type="danger" :disabled="!canDeleteByCreator(row)" :title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -177,6 +177,7 @@
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { driverApi, type Driver } from '@/api/pl'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
@@ -315,6 +316,10 @@ function handleDialogClose() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Driver) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该司机吗?', '提示', { type: 'warning' })
|
||||
await driverApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
<el-icon><Promotion /></el-icon>提交审批
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.settlementStatus === 'pending'"
|
||||
v-if="row.settlementStatus === 'pending' && canDeleteByCreator(row)"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
@@ -165,6 +165,7 @@ import {
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import { carrierApi, freightApi, type Carrier, type FreightSettlement } from '@/api/pl'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import PlFreightApprovalDialog from '@/components/business/pl-freight-approval-dialog/index.vue'
|
||||
import PlFreightSettlementFormDialog from '@/components/business/pl-freight-settlement-form-dialog/index.vue'
|
||||
import PlFreightSettlementViewDialog from '@/components/business/pl-freight-settlement-view-dialog/index.vue'
|
||||
@@ -269,6 +270,10 @@ async function handleWithdraw(row: FreightSettlement) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: FreightSettlement) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该草稿结算单吗?关联运输单将释放。', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
@@ -61,11 +61,6 @@
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stockAmount" label="库存金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatAmount(row.stockAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预警阈值" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.lowStockWarning || '-' }} ~ {{ row.highStockWarning || '-' }}
|
||||
@@ -108,7 +103,6 @@
|
||||
import { RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { inventoryApi, type InventoryRecord } from '@/api/pl'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
const loading = ref(false)
|
||||
const inventoryList = ref<InventoryRecord[]>([])
|
||||
@@ -123,11 +117,6 @@ const pagination = reactive({
|
||||
total: 0
|
||||
})
|
||||
|
||||
function formatAmount(amount: number) {
|
||||
if (!amount) return '¥0.00'
|
||||
return formatMoneyDisplay(amount)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
<el-button link type="primary" @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click.stop="handleDelete(row)">
|
||||
<el-button link type="danger" :disabled="!canDeleteByCreator(row)" :title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -219,6 +219,7 @@ import SystemViewDialog, {
|
||||
} from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
@@ -489,6 +490,10 @@ function handleDialogClose() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Personnel) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该人员吗?', '提示', { type: 'warning' })
|
||||
await personnelApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
@@ -154,11 +154,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalAmount" label="计划金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatAmount(row.totalAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="driverName" label="司机" min-width="100" align="center" />
|
||||
<el-table-column prop="followUserName" label="跟进人" min-width="100" align="center" />
|
||||
@@ -237,7 +232,6 @@ import {
|
||||
getPlPlanArchiveStatusLabel,
|
||||
getPlPlanArchiveStatusTagType
|
||||
} from '@/utils/pl-display'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
const loading = ref(false)
|
||||
const formDialogVisible = ref(false)
|
||||
@@ -299,11 +293,6 @@ function getFollowStatusLabel(status: string) {
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function formatAmount(amount: number) {
|
||||
if (!amount) return '¥0.00'
|
||||
return formatMoneyDisplay(amount)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
|
||||
@@ -132,11 +132,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalAmount" label="计划金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatAmount(row.totalAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="driverName" label="司机" min-width="100" align="center" />
|
||||
<el-table-column prop="followUserName" label="跟进人" min-width="100" align="center" />
|
||||
@@ -196,7 +191,6 @@ import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/ind
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
const loading = ref(false)
|
||||
const viewVisible = ref(false)
|
||||
@@ -254,11 +248,6 @@ function getFollowStatusLabel(status: string) {
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function formatAmount(amount: number) {
|
||||
if (!amount) return '¥0.00'
|
||||
return formatMoneyDisplay(amount)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
|
||||
@@ -103,7 +103,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="multiple"
|
||||
:disabled="multiple || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="deleteLoading"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
@@ -161,11 +162,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalAmount" label="计划金额" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatAmount(row.totalAmount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="driverName" label="司机" min-width="100" align="center" />
|
||||
<el-table-column prop="followUserName" label="跟进人" min-width="100" align="center" />
|
||||
@@ -201,7 +197,13 @@
|
||||
>
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -259,6 +261,7 @@ import {
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import { planApi, type Plan } from '@/api/pl'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import PlPlanArrangeDialog from '@/components/business/pl-plan-arrange-dialog/index.vue'
|
||||
import PlPlanCertUploadDialog from '@/components/business/pl-plan-cert-upload-dialog/index.vue'
|
||||
import PlPlanFormDialog from '@/components/business/pl-plan-form-dialog/index.vue'
|
||||
@@ -266,8 +269,6 @@ import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/ind
|
||||
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
|
||||
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -336,11 +337,6 @@ function getFollowStatusLabel(status: string) {
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
function formatAmount(amount: number) {
|
||||
if (!amount) return '¥0.00'
|
||||
return formatMoneyDisplay(amount)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -454,6 +450,10 @@ function handleArrange(row: Plan) {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Plan) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除运输计划「${row.planNo}」吗?`, '提示', { type: 'warning' })
|
||||
deleteLoading.value = true
|
||||
try {
|
||||
@@ -467,11 +467,16 @@ async function handleDelete(row: Plan) {
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map(r => r.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请至少选择一项')
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map(r => r.id)
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 条运输计划吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
@@ -24,6 +24,16 @@
|
||||
<el-option label="报废" value="scrapped" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="承运商">
|
||||
<el-select v-model="searchForm.carrierId" placeholder="全部" clearable filterable>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
:key="c.id"
|
||||
:label="c.carrierName"
|
||||
:value="c.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
@@ -84,6 +94,10 @@
|
||||
<dt>载重(吨)</dt>
|
||||
<dd>{{ row.loadCapacity || '—' }}</dd>
|
||||
</div>
|
||||
<div class="vehicle-card-kv-item">
|
||||
<dt>承运商</dt>
|
||||
<dd>{{ row.carrierName || '—' }}</dd>
|
||||
</div>
|
||||
<div class="vehicle-card-kv-item">
|
||||
<dt>关联人员</dt>
|
||||
<dd>{{ row.personnelSummary || '—' }}</dd>
|
||||
@@ -101,7 +115,7 @@
|
||||
<el-button link type="primary" @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click.stop="handleDelete(row)">
|
||||
<el-button link type="danger" :disabled="!canDeleteByCreator(row)" :title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -135,6 +149,16 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="承运商" prop="carrierId">
|
||||
<el-select v-model="formData.carrierId" placeholder="请选择承运商(选填)" clearable filterable>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
:key="c.id"
|
||||
:label="c.carrierName"
|
||||
:value="c.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车辆状态" prop="vehicleStatus">
|
||||
<el-select v-model="formData.vehicleStatus" placeholder="请选择">
|
||||
<el-option label="可用" value="available" />
|
||||
@@ -216,9 +240,11 @@
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<PlCertFormList
|
||||
<PlVehicleCertPartTabs
|
||||
ref="certFormListRef"
|
||||
v-model="certList"
|
||||
:license-plate="formData.licensePlate"
|
||||
:trailer-no="formData.trailerNo"
|
||||
title="车辆证书"
|
||||
upload-folder="pl/vehicle-cert"
|
||||
:cert-name-options="certNameOptions"
|
||||
@@ -250,6 +276,8 @@
|
||||
<template #vehicleDetailTabs>
|
||||
<PlVehicleCertPersonnelTabs
|
||||
:cert-list="viewDetail?.certList || []"
|
||||
:license-plate="viewDetail?.licensePlate"
|
||||
:trailer-no="viewDetail?.trailerNo"
|
||||
:personnel-list="viewPersonnelList"
|
||||
/>
|
||||
</template>
|
||||
@@ -262,13 +290,15 @@ import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Van, View } from
|
||||
import {
|
||||
vehicleApi,
|
||||
personnelApi,
|
||||
carrierApi,
|
||||
type Vehicle,
|
||||
type VehicleDetail,
|
||||
type VehiclePersonnelRow,
|
||||
type CertRow,
|
||||
type Personnel
|
||||
type Personnel,
|
||||
type Carrier
|
||||
} from '@/api/pl'
|
||||
import PlCertFormList from '@/components/business/pl-cert-form-list/index.vue'
|
||||
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
|
||||
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import SystemViewDialog, {
|
||||
@@ -281,15 +311,22 @@ import {
|
||||
getPlVehicleStatusLabel,
|
||||
getPlVehicleStatusTagType
|
||||
} from '@/utils/pl-display'
|
||||
import {
|
||||
PL_VEHICLE_CERT_NAME_OPTIONS,
|
||||
splitVehicleCertsByPart,
|
||||
validateVehicleRequiredCerts
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
const VEHICLE_TYPE_SELF = 'self'
|
||||
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
type CertFormListRef = InstanceType<typeof PlVehicleCertPartTabs>
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const vehicleList = ref<Vehicle[]>([])
|
||||
const carrierList = ref<Carrier[]>([])
|
||||
const personnelOptions = ref<Personnel[]>([])
|
||||
const selectedRows = ref<Vehicle[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
@@ -305,18 +342,12 @@ const viewLoading = ref(false)
|
||||
const viewDetail = ref<VehicleDetail | null>(null)
|
||||
const viewPersonnelList = ref<VehiclePersonnelViewRow[]>([])
|
||||
|
||||
const certNameOptions = [
|
||||
'行驶证',
|
||||
'道路运输证',
|
||||
'驾驶证',
|
||||
'押运员证',
|
||||
'危化品从业资格证',
|
||||
'保险单'
|
||||
]
|
||||
const certNameOptions = PL_VEHICLE_CERT_NAME_OPTIONS
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
vehicleStatus: ''
|
||||
vehicleStatus: '',
|
||||
carrierId: ''
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
@@ -324,6 +355,8 @@ const formData = reactive({
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
loadCapacity: null as number | null,
|
||||
carrierId: '',
|
||||
carrierName: '',
|
||||
vehicleStatus: 'available',
|
||||
remark: ''
|
||||
})
|
||||
@@ -343,7 +376,11 @@ function getStatusLabel(status: string) {
|
||||
|
||||
const viewHeroMeta = computed(() => {
|
||||
if (!viewDetail.value) return ''
|
||||
return [getStatusLabel(viewDetail.value.vehicleStatus), '自有']
|
||||
return [
|
||||
getStatusLabel(viewDetail.value.vehicleStatus),
|
||||
'自有',
|
||||
viewDetail.value.carrierName || undefined
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
})
|
||||
@@ -359,6 +396,7 @@ const viewSections = computed<ViewSection[]>(() => {
|
||||
{ label: '车牌号', value: d.licensePlate },
|
||||
{ label: '挂车号', value: d.trailerNo || '—' },
|
||||
{ label: '车辆类型', value: '自有', tagType: 'success' },
|
||||
{ label: '承运商', value: d.carrierName || '—' },
|
||||
{ label: '载重(吨)', value: d.loadCapacity ?? '—' },
|
||||
{
|
||||
label: '车辆状态',
|
||||
@@ -506,24 +544,33 @@ function validateSubTables(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const cert of certList.value) {
|
||||
if (!cert.certName?.trim()) {
|
||||
ElMessage.warning('证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
const hasTrailer = Boolean(formData.trailerNo?.trim())
|
||||
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList.value)
|
||||
return validateVehicleRequiredCerts({
|
||||
headCerts,
|
||||
trailerCerts,
|
||||
hasTrailer,
|
||||
licensePlate: formData.licensePlate,
|
||||
trailerNo: formData.trailerNo,
|
||||
onError: message => ElMessage.warning(message)
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleApi.list({
|
||||
vehicleType: VEHICLE_TYPE_SELF,
|
||||
q: searchForm.q || undefined,
|
||||
vehicleStatus: searchForm.vehicleStatus || undefined
|
||||
})
|
||||
vehicleList.value = (res.data ?? res) || []
|
||||
const [vehicleRes, carrierRes] = await Promise.all([
|
||||
vehicleApi.list({
|
||||
vehicleType: VEHICLE_TYPE_SELF,
|
||||
q: searchForm.q || undefined,
|
||||
vehicleStatus: searchForm.vehicleStatus || undefined,
|
||||
carrierId: searchForm.carrierId || undefined
|
||||
}),
|
||||
carrierApi.list()
|
||||
])
|
||||
vehicleList.value = (vehicleRes.data ?? vehicleRes) || []
|
||||
carrierList.value = (carrierRes.data ?? carrierRes) || []
|
||||
const idSet = new Set(vehicleList.value.map(item => item.id))
|
||||
selectedRows.value = selectedRows.value.filter(item => idSet.has(item.id))
|
||||
} finally {
|
||||
@@ -538,6 +585,7 @@ function handleSearch() {
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.vehicleStatus = ''
|
||||
searchForm.carrierId = ''
|
||||
selectedRows.value = []
|
||||
fetchData()
|
||||
}
|
||||
@@ -564,6 +612,8 @@ function handleAdd() {
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
loadCapacity: null,
|
||||
carrierId: '',
|
||||
carrierName: '',
|
||||
vehicleStatus: 'available',
|
||||
remark: ''
|
||||
})
|
||||
@@ -608,6 +658,8 @@ async function handleEdit(row: Vehicle) {
|
||||
licensePlate: detail.licensePlate,
|
||||
trailerNo: detail.trailerNo || '',
|
||||
loadCapacity: detail.loadCapacity ?? null,
|
||||
carrierId: detail.carrierId || '',
|
||||
carrierName: detail.carrierName || '',
|
||||
vehicleStatus: detail.vehicleStatus,
|
||||
remark: detail.remark
|
||||
})
|
||||
@@ -620,6 +672,7 @@ async function handleEdit(row: Vehicle) {
|
||||
certList.value = (detail.certList || []).map(c => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate,
|
||||
@@ -641,12 +694,21 @@ async function handleSubmit() {
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.carrierId) {
|
||||
const carrier = carrierList.value.find(c => c.id === formData.carrierId)
|
||||
formData.carrierName = carrier?.carrierName || ''
|
||||
} else {
|
||||
formData.carrierName = ''
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
licensePlate: formData.licensePlate,
|
||||
trailerNo: formData.trailerNo || undefined,
|
||||
vehicleType: VEHICLE_TYPE_SELF,
|
||||
carrierId: formData.carrierId || undefined,
|
||||
carrierName: formData.carrierName || undefined,
|
||||
loadCapacity: formData.loadCapacity,
|
||||
vehicleStatus: formData.vehicleStatus,
|
||||
remark: formData.remark,
|
||||
@@ -661,6 +723,7 @@ async function handleSubmit() {
|
||||
})),
|
||||
certList: certList.value.map(c => ({
|
||||
certId: c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName.trim(),
|
||||
certNo: c.certNo || undefined,
|
||||
expireDate: c.expireDate || undefined,
|
||||
@@ -688,6 +751,10 @@ function handleDialogClose() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Vehicle) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该自有车辆吗?', '提示', { type: 'warning' })
|
||||
await vehicleApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
<!-- 车辆维保申请 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">车辆维保申请</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="申请单号/车牌">
|
||||
<el-input v-model="searchForm.q" placeholder="搜索申请单号或车牌" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="维保类型">
|
||||
<el-select
|
||||
v-model="searchForm.maintenanceType"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in PL_MAINTENANCE_TYPE_PRESETS"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||||
<el-option label="待处理" value="pending" />
|
||||
<el-option label="已处理" value="processed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增申请
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="maintenanceType" label="维保类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlMaintenanceTypeTagType(row.maintenanceType)" size="small">
|
||||
{{ getPlMaintenanceTypeLabel(row.maintenanceType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyDate" label="申请日期" min-width="120" align="center" />
|
||||
<el-table-column prop="startDate" label="开始日期" min-width="120" align="center" />
|
||||
<el-table-column prop="expectedPickupDate" label="预计取车" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.expectedPickupDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="status" label="状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlMaintenanceApplyStatusTagType(row.status)" size="small">
|
||||
{{ getPlMaintenanceApplyStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<template v-if="row.status === 'pending'">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="warning" :loading="cancelLoadingMap[row.id]" @click="handleCancel(row)">
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button link type="danger" :loading="deleteLoadingMap[row.id]" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item v-if="formData.applyNo" label="申请单号">
|
||||
<el-input v-model="formData.applyNo" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请人" prop="applicantId">
|
||||
<el-select
|
||||
v-model="formData.applicantId"
|
||||
filterable
|
||||
placeholder="请选择申请人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.displayName"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
@change="handleLicensePlateChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="维保类型" prop="maintenanceType">
|
||||
<el-select
|
||||
v-model="formData.maintenanceType"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="请选择或输入维保类型"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in PL_MAINTENANCE_TYPE_PRESETS"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期" prop="applyDate">
|
||||
<el-date-picker v-model="formData.applyDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开始日期" prop="startDate">
|
||||
<el-date-picker v-model="formData.startDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预计取车日期">
|
||||
<el-date-picker
|
||||
v-model="formData.expectedPickupDate"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="可选"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="维保内容" prop="maintenanceContent" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="formData.maintenanceContent"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入维保内容"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="维修店地址" class="form-col-span-2">
|
||||
<el-input v-model="formData.shopAddress" placeholder="请输入维修店地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-maintenance/apply"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewDetail?.applyNo || '申请详情'"
|
||||
:sections="viewSections"
|
||||
:loading="viewLoading"
|
||||
width="720px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Check,
|
||||
CircleClose,
|
||||
Close,
|
||||
Delete,
|
||||
Edit,
|
||||
Plus,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleMaintenanceApplyApi,
|
||||
type Vehicle,
|
||||
type VehicleMaintenanceApply
|
||||
} from '@/api/pl'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
getPlMaintenanceApplyStatusLabel,
|
||||
getPlMaintenanceApplyStatusTagType,
|
||||
getPlMaintenanceTypeLabel,
|
||||
getPlMaintenanceTypeTagType,
|
||||
PL_MAINTENANCE_TYPE_PRESETS
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
})
|
||||
|
||||
const userOptions = ref<
|
||||
Array<{ userId: string; nickName: string; displayName: string }>
|
||||
>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const cancelLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleMaintenanceApply[]>([])
|
||||
const selectedRows = ref<VehicleMaintenanceApply[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
maintenanceType: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
applyNo: '',
|
||||
applicantId: '',
|
||||
licensePlate: '',
|
||||
maintenanceType: '',
|
||||
applyDate: '',
|
||||
startDate: '',
|
||||
expectedPickupDate: '',
|
||||
maintenanceContent: '',
|
||||
shopAddress: '',
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
applicantId: [{ required: true, message: '请选择申请人', trigger: 'change' }],
|
||||
licensePlate: [{ required: true, message: '请选择车牌号', trigger: 'change' }],
|
||||
maintenanceType: [{ required: true, message: '请选择维保类型', trigger: 'change' }],
|
||||
applyDate: [{ required: true, message: '请选择申请日期', trigger: 'change' }],
|
||||
startDate: [{ required: true, message: '请选择开始日期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewLoading = ref(false)
|
||||
const viewDetail = ref<VehicleMaintenanceApply | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
return [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: d.applyNo },
|
||||
{ label: '车牌号', value: d.licensePlate },
|
||||
{
|
||||
label: '维保类型',
|
||||
value: getPlMaintenanceTypeLabel(d.maintenanceType),
|
||||
tagType: getPlMaintenanceTypeTagType(d.maintenanceType)
|
||||
},
|
||||
{ label: '申请日期', value: d.applyDate },
|
||||
{ label: '开始日期', value: d.startDate },
|
||||
{ label: '预计取车', value: d.expectedPickupDate || '—' },
|
||||
{ label: '申请人', value: d.applicantName || '—' },
|
||||
{
|
||||
label: '状态',
|
||||
value: getPlMaintenanceApplyStatusLabel(d.status),
|
||||
tagType: getPlMaintenanceApplyStatusTagType(d.status)
|
||||
},
|
||||
{ label: '维保内容', value: d.maintenanceContent || '—', span: 2 },
|
||||
{ label: '维修店地址', value: d.shopAddress || '—', span: 2 },
|
||||
{ label: '备注', value: d.remark || '—', span: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: unknown = (res as { data?: unknown })?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: Record<string, unknown>) => {
|
||||
const name = String(u.label || u.nickName || '')
|
||||
const account = String(u.userNo || '')
|
||||
return {
|
||||
userId: String(u.value ?? u.id ?? ''),
|
||||
nickName: name,
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list()
|
||||
vehicleOptions.value = (res.data ?? res) || []
|
||||
}
|
||||
|
||||
async function showMaintenanceReminders(licensePlate: string) {
|
||||
if (!licensePlate) return
|
||||
try {
|
||||
const res = await vehicleMaintenanceApplyApi.reminders(
|
||||
licensePlate,
|
||||
isEdit.value ? formData.id : undefined
|
||||
)
|
||||
const items = (res.data ?? res) || []
|
||||
if (!items.length) return
|
||||
const text = items
|
||||
.map((item, index) => {
|
||||
const lines = [
|
||||
`【${index + 1}】申请单号:${item.applyNo}`,
|
||||
`维保类型:${item.maintenanceType}`,
|
||||
`开始日期:${item.startDate}`,
|
||||
`预计取车:${item.expectedPickupDate || '—'}`,
|
||||
`维保内容:${item.maintenanceContent || '—'}`,
|
||||
`维修店地址:${item.shopAddress || '—'}`
|
||||
]
|
||||
return lines.join('\n')
|
||||
})
|
||||
.join('\n\n')
|
||||
await ElMessageBox.alert(text, '车辆维保提醒', { confirmButtonText: '知道了' })
|
||||
} catch {
|
||||
// ignore reminder fetch errors
|
||||
}
|
||||
}
|
||||
|
||||
function handleLicensePlateChange(licensePlate: string) {
|
||||
showMaintenanceReminders(licensePlate)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleMaintenanceApplyApi.list({
|
||||
q: searchForm.q || undefined,
|
||||
maintenanceType: searchForm.maintenanceType || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
applyDateFrom: dateRange.value?.[0],
|
||||
applyDateTo: dateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.maintenanceType = ''
|
||||
searchForm.status = ''
|
||||
dateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: VehicleMaintenanceApply[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.applyNo = ''
|
||||
formData.applicantId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.maintenanceType = ''
|
||||
formData.applyDate = ''
|
||||
formData.startDate = ''
|
||||
formData.expectedPickupDate = ''
|
||||
formData.maintenanceContent = ''
|
||||
formData.shopAddress = ''
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
resetForm()
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增申请'
|
||||
formData.applyDate = new Date().toISOString().slice(0, 10)
|
||||
formData.applicantId = currentUserId.value
|
||||
await loadUserOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleMaintenanceApply) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑申请'
|
||||
await loadUserOptions()
|
||||
Object.assign(formData, {
|
||||
id: row.id,
|
||||
applyNo: row.applyNo,
|
||||
applicantId: row.applicantId || '',
|
||||
licensePlate: row.licensePlate,
|
||||
maintenanceType: row.maintenanceType,
|
||||
applyDate: row.applyDate,
|
||||
startDate: row.startDate,
|
||||
expectedPickupDate: row.expectedPickupDate || '',
|
||||
maintenanceContent: row.maintenanceContent || '',
|
||||
shopAddress: row.shopAddress || '',
|
||||
attachmentUrls: row.attachmentUrls || '',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleView(row: VehicleMaintenanceApply) {
|
||||
viewVisible.value = true
|
||||
viewLoading.value = true
|
||||
try {
|
||||
const res = await vehicleMaintenanceApplyApi.get(row.id)
|
||||
viewDetail.value = res.data ?? res
|
||||
} finally {
|
||||
viewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applicantId: formData.applicantId,
|
||||
licensePlate: formData.licensePlate,
|
||||
maintenanceType: formData.maintenanceType,
|
||||
applyDate: formData.applyDate,
|
||||
startDate: formData.startDate,
|
||||
expectedPickupDate: formData.expectedPickupDate || undefined,
|
||||
maintenanceContent: formData.maintenanceContent || undefined,
|
||||
shopAddress: formData.shopAddress || undefined,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await vehicleMaintenanceApplyApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleMaintenanceApplyApi.create(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel(row: VehicleMaintenanceApply) {
|
||||
await ElMessageBox.confirm('确定作废该申请吗?', '提示', { type: 'warning' })
|
||||
cancelLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleMaintenanceApplyApi.cancel(row.id)
|
||||
ElMessage.success('已作废')
|
||||
fetchData()
|
||||
} finally {
|
||||
cancelLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleMaintenanceApply) {
|
||||
await ElMessageBox.confirm('确定删除该申请吗?', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleMaintenanceApplyApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadVehicleOptions(), loadUserOptions()])
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,586 @@
|
||||
<!-- 车辆维保处理登记 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">车辆维保处理登记</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="申请单号/车牌/处理单号">
|
||||
<el-input v-model="searchForm.q" placeholder="搜索关键词" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="维保类型">
|
||||
<el-select
|
||||
v-model="searchForm.maintenanceType"
|
||||
placeholder="全部"
|
||||
clearable
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in PL_MAINTENANCE_TYPE_PRESETS"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:value="item"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请状态">
|
||||
<el-select v-model="searchForm.applyStatus" placeholder="全部" clearable>
|
||||
<el-option label="待处理" value="pending" />
|
||||
<el-option label="已处理" value="processed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="applyDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="处理日期">
|
||||
<el-date-picker
|
||||
v-model="processDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">共 <span class="sel-num">{{ pagination.total }}</span> 条申请</div>
|
||||
<el-button @click="handleSupplement">
|
||||
<el-icon><Plus /></el-icon>无申请补录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="applyId">
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip fixed="left" />
|
||||
<el-table-column prop="applyDate" label="申请日期" min-width="120" align="center" />
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="applyStatus" label="申请状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlMaintenanceApplyStatusTagType(row.applyStatus)" size="small">
|
||||
{{ getPlMaintenanceApplyStatusLabel(row.applyStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="110" align="center" />
|
||||
<el-table-column prop="maintenanceType" label="维保类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlMaintenanceTypeTagType(row.maintenanceType)" size="small">
|
||||
{{ getPlMaintenanceTypeLabel(row.maintenanceType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startDate" label="开始日期" min-width="120" align="center" />
|
||||
<el-table-column prop="expectedPickupDate" label="预计取车" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.expectedPickupDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="processNo" label="处理单号" min-width="170" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.processNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="processDate" label="处理日期" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.processDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额(元)" min-width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.amount != null ? Number(row.amount).toFixed(2) : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="handlerName" label="处理人" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="260" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.applyStatus === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleProcess(row)"
|
||||
>
|
||||
<el-icon><Tools /></el-icon>处理
|
||||
</el-button>
|
||||
<template v-if="row.applyStatus === 'processed' && row.processId">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:loading="deleteLoadingMap[row.processId]"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>撤销处理
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" scroll-to-error>
|
||||
<div v-if="dialogMode === 'process' && currentApplyRow" class="apply-summary-block">
|
||||
<div class="apply-summary-title">关联申请</div>
|
||||
<div class="apply-summary-grid">
|
||||
<div><span class="label">申请单号</span>{{ currentApplyRow.applyNo }}</div>
|
||||
<div><span class="label">申请日期</span>{{ currentApplyRow.applyDate }}</div>
|
||||
<div><span class="label">车牌号</span>{{ currentApplyRow.licensePlate }}</div>
|
||||
<div>
|
||||
<span class="label">维保类型</span>
|
||||
{{ getPlMaintenanceTypeLabel(currentApplyRow.maintenanceType) }}
|
||||
</div>
|
||||
<div><span class="label">申请人</span>{{ currentApplyRow.applicantName || '—' }}</div>
|
||||
<div><span class="label">开始日期</span>{{ currentApplyRow.startDate }}</div>
|
||||
<div>
|
||||
<span class="label">预计取车</span>{{ currentApplyRow.expectedPickupDate || '—' }}
|
||||
</div>
|
||||
<div class="apply-summary-span-2">
|
||||
<span class="label">维保内容</span>{{ currentApplyRow.maintenanceContent || '—' }}
|
||||
</div>
|
||||
<div class="apply-summary-span-2">
|
||||
<span class="label">维修店地址</span>{{ currentApplyRow.shopAddress || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="处理单号">
|
||||
<el-input :model-value="formData.processNo || '保存后自动生成'" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="处理人" prop="handlerId">
|
||||
<el-select
|
||||
v-model="formData.handlerId"
|
||||
filterable
|
||||
placeholder="请选择处理人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.displayName"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="dialogMode === 'supplement'">
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
@change="handleLicensePlateChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="处理日期" prop="processDate">
|
||||
<el-date-picker v-model="formData.processDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="金额(元)">
|
||||
<el-input-number
|
||||
v-model="formData.amount"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
placeholder="可选"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-maintenance/process"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Tools } from '@element-plus/icons-vue'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleMaintenanceApplyApi,
|
||||
vehicleMaintenanceProcessApi,
|
||||
type Vehicle,
|
||||
type VehicleMaintenanceProcessBoardItem
|
||||
} from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
getPlMaintenanceApplyStatusLabel,
|
||||
getPlMaintenanceApplyStatusTagType,
|
||||
getPlMaintenanceTypeLabel,
|
||||
getPlMaintenanceTypeTagType,
|
||||
PL_MAINTENANCE_TYPE_PRESETS
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
type DialogMode = 'process' | 'edit' | 'supplement'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
})
|
||||
|
||||
const userOptions = ref<
|
||||
Array<{ userId: string; nickName: string; displayName: string }>
|
||||
>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleMaintenanceProcessBoardItem[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const applyDateRange = ref<string[]>([])
|
||||
const processDateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
maintenanceType: '',
|
||||
applyStatus: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const dialogMode = ref<DialogMode>('process')
|
||||
const currentApplyRow = ref<VehicleMaintenanceProcessBoardItem | null>(null)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
processNo: '',
|
||||
applyId: '',
|
||||
licensePlate: '',
|
||||
handlerId: '',
|
||||
processDate: '',
|
||||
amount: null as number | null,
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = computed(() => ({
|
||||
licensePlate:
|
||||
dialogMode.value === 'supplement'
|
||||
? [{ required: true, message: '请选择车牌号', trigger: 'change' }]
|
||||
: [],
|
||||
handlerId: [{ required: true, message: '请选择处理人', trigger: 'change' }],
|
||||
processDate: [{ required: true, message: '请选择处理日期', trigger: 'change' }]
|
||||
}))
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: unknown = (res as { data?: unknown })?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: Record<string, unknown>) => {
|
||||
const name = String(u.label || u.nickName || '')
|
||||
const account = String(u.userNo || '')
|
||||
return {
|
||||
userId: String(u.value ?? u.id ?? ''),
|
||||
nickName: name,
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list()
|
||||
vehicleOptions.value = (res.data ?? res) || []
|
||||
}
|
||||
|
||||
async function showMaintenanceReminders(licensePlate: string) {
|
||||
if (!licensePlate) return
|
||||
try {
|
||||
const res = await vehicleMaintenanceApplyApi.reminders(licensePlate)
|
||||
const items = (res.data ?? res) || []
|
||||
if (!items.length) return
|
||||
const text = items
|
||||
.map((item, index) => {
|
||||
const lines = [
|
||||
`【${index + 1}】申请单号:${item.applyNo}`,
|
||||
`维保类型:${item.maintenanceType}`,
|
||||
`开始日期:${item.startDate}`,
|
||||
`预计取车:${item.expectedPickupDate || '—'}`,
|
||||
`维保内容:${item.maintenanceContent || '—'}`,
|
||||
`维修店地址:${item.shopAddress || '—'}`
|
||||
]
|
||||
return lines.join('\n')
|
||||
})
|
||||
.join('\n\n')
|
||||
await ElMessageBox.alert(text, '车辆维保提醒', { confirmButtonText: '知道了' })
|
||||
} catch {
|
||||
// ignore reminder fetch errors
|
||||
}
|
||||
}
|
||||
|
||||
function handleLicensePlateChange(licensePlate: string) {
|
||||
showMaintenanceReminders(licensePlate)
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleMaintenanceProcessApi.processBoard({
|
||||
q: searchForm.q || undefined,
|
||||
maintenanceType: searchForm.maintenanceType || undefined,
|
||||
applyStatus: searchForm.applyStatus || undefined,
|
||||
applyDateFrom: applyDateRange.value?.[0],
|
||||
applyDateTo: applyDateRange.value?.[1],
|
||||
processDateFrom: processDateRange.value?.[0],
|
||||
processDateTo: processDateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.maintenanceType = ''
|
||||
searchForm.applyStatus = ''
|
||||
applyDateRange.value = []
|
||||
processDateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.processNo = ''
|
||||
formData.applyId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.handlerId = ''
|
||||
formData.processDate = ''
|
||||
formData.amount = null
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
currentApplyRow.value = null
|
||||
}
|
||||
|
||||
async function openDialogWithDefaults(mode: DialogMode, title: string) {
|
||||
dialogMode.value = mode
|
||||
dialogTitle.value = title
|
||||
formData.processDate = new Date().toISOString().slice(0, 10)
|
||||
formData.handlerId = currentUserId.value
|
||||
await loadUserOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleProcess(row: VehicleMaintenanceProcessBoardItem) {
|
||||
resetForm()
|
||||
currentApplyRow.value = row
|
||||
formData.applyId = row.applyId
|
||||
formData.licensePlate = row.licensePlate
|
||||
await openDialogWithDefaults('process', '处理登记')
|
||||
}
|
||||
|
||||
async function handleSupplement() {
|
||||
resetForm()
|
||||
await loadVehicleOptions()
|
||||
await openDialogWithDefaults('supplement', '无申请补录')
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleMaintenanceProcessBoardItem) {
|
||||
if (!row.processId) return
|
||||
resetForm()
|
||||
dialogMode.value = 'edit'
|
||||
dialogTitle.value = '编辑处理登记'
|
||||
currentApplyRow.value = row
|
||||
await loadUserOptions()
|
||||
const res = await vehicleMaintenanceProcessApi.get(row.processId)
|
||||
const detail = res.data ?? res
|
||||
Object.assign(formData, {
|
||||
id: detail.id,
|
||||
processNo: detail.processNo,
|
||||
applyId: detail.applyId || row.applyId,
|
||||
licensePlate: detail.licensePlate || row.licensePlate,
|
||||
handlerId: detail.handlerId,
|
||||
processDate: detail.processDate,
|
||||
amount: detail.amount != null ? Number(detail.amount) : null,
|
||||
attachmentUrls: detail.attachmentUrls || '',
|
||||
remark: detail.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applyId: formData.applyId || undefined,
|
||||
licensePlate: dialogMode.value === 'supplement' ? formData.licensePlate : undefined,
|
||||
handlerId: formData.handlerId,
|
||||
processDate: formData.processDate,
|
||||
amount: formData.amount ?? undefined,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (dialogMode.value === 'edit') {
|
||||
await vehicleMaintenanceProcessApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleMaintenanceProcessApi.create(payload)
|
||||
ElMessage.success('处理登记成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleMaintenanceProcessBoardItem) {
|
||||
if (!row.processId) return
|
||||
await ElMessageBox.confirm('确定撤销该处理登记吗?关联申请将回退为待处理。', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.processId] = true
|
||||
try {
|
||||
await vehicleMaintenanceProcessApi.delete(row.processId)
|
||||
ElMessage.success('已撤销处理')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.processId] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.apply-summary-block {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--primary-bg);
|
||||
}
|
||||
|
||||
.apply-summary-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.apply-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
min-width: 72px;
|
||||
margin-right: 6px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.apply-summary-span-2 {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,508 @@
|
||||
<!-- ETC加油申请 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">ETC加油申请</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="申请单号/车牌">
|
||||
<el-input v-model="searchForm.q" placeholder="搜索申请单号或车牌" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="充值类型">
|
||||
<el-select v-model="searchForm.rechargeType" placeholder="全部" clearable>
|
||||
<el-option label="ETC" value="etc" />
|
||||
<el-option label="加油" value="fuel" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||||
<el-option label="待发放" value="pending" />
|
||||
<el-option label="已发放" value="disbursed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增申请
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="rechargeType" label="充值类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlRechargeTypeTagType(row.rechargeType)" size="small">
|
||||
{{ getPlRechargeTypeLabel(row.rechargeType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyDate" label="申请日期" min-width="120" align="center" />
|
||||
<el-table-column prop="odometerKm" label="期初公里数" min-width="110" align="center">
|
||||
<template #default="{ row }">{{ row.odometerKm ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="status" label="状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlRechargeApplyStatusTagType(row.status)" size="small">
|
||||
{{ getPlRechargeApplyStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<template v-if="row.status === 'pending'">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="warning" :loading="cancelLoadingMap[row.id]" @click="handleCancel(row)">
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button link type="danger" :loading="deleteLoadingMap[row.id]" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item v-if="formData.applyNo" label="申请单号">
|
||||
<el-input v-model="formData.applyNo" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请人" prop="applicantId">
|
||||
<el-select
|
||||
v-model="formData.applicantId"
|
||||
filterable
|
||||
placeholder="请选择申请人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.displayName"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select v-model="formData.licensePlate" filterable placeholder="请选择车牌" style="width: 100%">
|
||||
<el-option v-for="item in vehicleOptions" :key="item.id" :label="item.licensePlate" :value="item.licensePlate" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值类型" prop="rechargeType">
|
||||
<el-select v-model="formData.rechargeType" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="ETC" value="etc" />
|
||||
<el-option label="加油" value="fuel" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期" prop="applyDate">
|
||||
<el-date-picker v-model="formData.applyDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="期初公里数" prop="odometerKm">
|
||||
<el-input-number v-model="formData.odometerKm" :min="0" :precision="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-recharge/apply"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewDetail?.applyNo || '申请详情'"
|
||||
:sections="viewSections"
|
||||
:loading="viewLoading"
|
||||
width="720px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Check,
|
||||
CircleClose,
|
||||
Close,
|
||||
Delete,
|
||||
Edit,
|
||||
Plus,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleRechargeApplyApi,
|
||||
type Vehicle,
|
||||
type VehicleRechargeApply
|
||||
} from '@/api/pl'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
getPlRechargeApplyStatusLabel,
|
||||
getPlRechargeApplyStatusTagType,
|
||||
getPlRechargeTypeLabel,
|
||||
getPlRechargeTypeTagType
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
})
|
||||
|
||||
const userOptions = ref<
|
||||
Array<{ userId: string; nickName: string; displayName: string }>
|
||||
>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const cancelLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleRechargeApply[]>([])
|
||||
const selectedRows = ref<VehicleRechargeApply[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
rechargeType: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
applyNo: '',
|
||||
applicantId: '',
|
||||
licensePlate: '',
|
||||
rechargeType: 'etc' as 'etc' | 'fuel',
|
||||
applyDate: '',
|
||||
odometerKm: null as number | null,
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
applicantId: [{ required: true, message: '请选择申请人', trigger: 'change' }],
|
||||
licensePlate: [{ required: true, message: '请选择车牌号', trigger: 'change' }],
|
||||
rechargeType: [{ required: true, message: '请选择充值类型', trigger: 'change' }],
|
||||
applyDate: [{ required: true, message: '请选择申请日期', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewLoading = ref(false)
|
||||
const viewDetail = ref<VehicleRechargeApply | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
return [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: d.applyNo },
|
||||
{ label: '车牌号', value: d.licensePlate },
|
||||
{
|
||||
label: '充值类型',
|
||||
value: getPlRechargeTypeLabel(d.rechargeType),
|
||||
tagType: getPlRechargeTypeTagType(d.rechargeType)
|
||||
},
|
||||
{ label: '申请日期', value: d.applyDate },
|
||||
{ label: '期初公里数', value: d.odometerKm != null ? String(d.odometerKm) : '—' },
|
||||
{ label: '申请人', value: d.applicantName || '—' },
|
||||
{
|
||||
label: '状态',
|
||||
value: getPlRechargeApplyStatusLabel(d.status),
|
||||
tagType: getPlRechargeApplyStatusTagType(d.status)
|
||||
},
|
||||
{ label: '备注', value: d.remark || '—', span: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: unknown = (res as { data?: unknown })?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: Record<string, unknown>) => {
|
||||
const name = String(u.label || u.nickName || '')
|
||||
const account = String(u.userNo || '')
|
||||
return {
|
||||
userId: String(u.value ?? u.id ?? ''),
|
||||
nickName: name,
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list()
|
||||
vehicleOptions.value = (res.data ?? res) || []
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleRechargeApplyApi.list({
|
||||
q: searchForm.q || undefined,
|
||||
rechargeType: searchForm.rechargeType || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
applyDateFrom: dateRange.value?.[0],
|
||||
applyDateTo: dateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.rechargeType = ''
|
||||
searchForm.status = ''
|
||||
dateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: VehicleRechargeApply[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.applyNo = ''
|
||||
formData.applicantId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.rechargeType = 'etc'
|
||||
formData.applyDate = ''
|
||||
formData.odometerKm = null
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
resetForm()
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增申请'
|
||||
formData.applyDate = new Date().toISOString().slice(0, 10)
|
||||
formData.applicantId = currentUserId.value
|
||||
await loadUserOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleRechargeApply) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑申请'
|
||||
await loadUserOptions()
|
||||
Object.assign(formData, {
|
||||
id: row.id,
|
||||
applyNo: row.applyNo,
|
||||
applicantId: row.applicantId || '',
|
||||
licensePlate: row.licensePlate,
|
||||
rechargeType: row.rechargeType,
|
||||
applyDate: row.applyDate,
|
||||
odometerKm: row.odometerKm ?? null,
|
||||
attachmentUrls: row.attachmentUrls || '',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleView(row: VehicleRechargeApply) {
|
||||
viewVisible.value = true
|
||||
viewLoading.value = true
|
||||
try {
|
||||
const res = await vehicleRechargeApplyApi.get(row.id)
|
||||
viewDetail.value = res.data ?? res
|
||||
} finally {
|
||||
viewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applicantId: formData.applicantId,
|
||||
licensePlate: formData.licensePlate,
|
||||
rechargeType: formData.rechargeType,
|
||||
applyDate: formData.applyDate,
|
||||
odometerKm: formData.odometerKm,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await vehicleRechargeApplyApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleRechargeApplyApi.create(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel(row: VehicleRechargeApply) {
|
||||
await ElMessageBox.confirm('确定作废该申请吗?', '提示', { type: 'warning' })
|
||||
cancelLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleRechargeApplyApi.cancel(row.id)
|
||||
ElMessage.success('已作废')
|
||||
fetchData()
|
||||
} finally {
|
||||
cancelLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleRechargeApply) {
|
||||
await ElMessageBox.confirm('确定删除该申请吗?', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleRechargeApplyApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadVehicleOptions(), loadUserOptions()])
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,604 @@
|
||||
<!-- ETC加油发放登记 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">ETC加油发放登记</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="申请单号/车牌/订单号">
|
||||
<el-input v-model="searchForm.q" placeholder="搜索关键词" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="充值类型">
|
||||
<el-select v-model="searchForm.rechargeType" placeholder="全部" clearable>
|
||||
<el-option label="ETC" value="etc" />
|
||||
<el-option label="加油" value="fuel" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请状态">
|
||||
<el-select v-model="searchForm.applyStatus" placeholder="全部" clearable>
|
||||
<el-option label="待发放" value="pending" />
|
||||
<el-option label="已发放" value="disbursed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="applyDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值日期">
|
||||
<el-date-picker
|
||||
v-model="rechargeDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">共 <span class="sel-num">{{ pagination.total }}</span> 条申请</div>
|
||||
<el-button @click="handleSupplement">
|
||||
<el-icon><Plus /></el-icon>无申请补录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="applyId">
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip fixed="left" />
|
||||
<el-table-column prop="applyDate" label="申请日期" min-width="120" align="center" />
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="odometerKm" label="期初公里数" min-width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.odometerKm != null ? Number(row.odometerKm).toFixed(1) : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyStatus" label="申请状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlRechargeApplyStatusTagType(row.applyStatus)" size="small">
|
||||
{{ getPlRechargeApplyStatusLabel(row.applyStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="110" align="center" />
|
||||
<el-table-column prop="rechargeType" label="充值类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlRechargeTypeTagType(row.rechargeType)" size="small">
|
||||
{{ getPlRechargeTypeLabel(row.rechargeType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rechargeOrderNo" label="充值订单号" min-width="170" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.rechargeOrderNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rechargeDate" label="充值日期" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.rechargeDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="cardNo" label="卡号" min-width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.cardNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="gasStation" label="加油站/渠道" min-width="130" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.gasStation || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额(元)" min-width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.amount != null ? Number(row.amount).toFixed(2) : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="recordCreateByName" label="登记人" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ row.recordCreateByName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="300" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.applyStatus === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDisburse(row)"
|
||||
>
|
||||
<el-icon><Money /></el-icon>发放
|
||||
</el-button>
|
||||
<template v-if="row.applyStatus === 'disbursed' && row.recordId">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:loading="deleteLoadingMap[row.recordId]"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>撤销发放
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" scroll-to-error>
|
||||
<div v-if="dialogMode === 'disburse' && currentApplyRow" class="apply-summary-block">
|
||||
<div class="apply-summary-title">关联申请</div>
|
||||
<div class="apply-summary-grid">
|
||||
<div><span class="label">申请单号</span>{{ currentApplyRow.applyNo }}</div>
|
||||
<div><span class="label">申请日期</span>{{ currentApplyRow.applyDate }}</div>
|
||||
<div><span class="label">车牌号</span>{{ currentApplyRow.licensePlate }}</div>
|
||||
<div>
|
||||
<span class="label">充值类型</span>
|
||||
{{ getPlRechargeTypeLabel(currentApplyRow.rechargeType) }}
|
||||
</div>
|
||||
<div><span class="label">申请人</span>{{ currentApplyRow.applicantName || '—' }}</div>
|
||||
<div>
|
||||
<span class="label">期初公里数</span>
|
||||
{{
|
||||
currentApplyRow.odometerKm != null
|
||||
? `${Number(currentApplyRow.odometerKm).toFixed(1)} km`
|
||||
: '—'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="充值订单号">
|
||||
<el-input :model-value="formData.rechargeOrderNo || '保存后自动生成'" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="登记人">
|
||||
<el-input :model-value="currentUserName" disabled />
|
||||
</el-form-item>
|
||||
<template v-if="dialogMode === 'supplement'">
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值类型" prop="rechargeType">
|
||||
<el-select v-model="formData.rechargeType" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="ETC" value="etc" />
|
||||
<el-option label="加油" value="fuel" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="充值日期" prop="rechargeDate">
|
||||
<el-date-picker v-model="formData.rechargeDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="卡号" prop="cardNo">
|
||||
<el-input v-model="formData.cardNo" placeholder="ETC卡号/油卡号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="加油站/渠道" prop="gasStation">
|
||||
<el-select
|
||||
v-model="formData.gasStation"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in PL_GAS_STATION_PRESETS" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值金额(元)" prop="amount">
|
||||
<el-input-number v-model="formData.amount" :min="0.01" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-recharge/record"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewRow?.applyNo || '发放详情'"
|
||||
:sections="viewSections"
|
||||
width="760px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Edit, Money, Plus, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleRechargeRecordApi,
|
||||
type Vehicle,
|
||||
type VehicleRechargeDisburseBoardItem
|
||||
} from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
getPlRechargeApplyStatusLabel,
|
||||
getPlRechargeApplyStatusTagType,
|
||||
getPlRechargeTypeLabel,
|
||||
getPlRechargeTypeTagType,
|
||||
PL_GAS_STATION_PRESETS
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
type DialogMode = 'disburse' | 'edit' | 'supplement'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const currentUserName = computed(() => userStore.info?.userName || '—')
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleRechargeDisburseBoardItem[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const applyDateRange = ref<string[]>([])
|
||||
const rechargeDateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
rechargeType: '',
|
||||
applyStatus: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const dialogMode = ref<DialogMode>('disburse')
|
||||
const currentApplyRow = ref<VehicleRechargeDisburseBoardItem | null>(null)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
rechargeOrderNo: '',
|
||||
applyId: '',
|
||||
licensePlate: '',
|
||||
rechargeType: 'etc' as 'etc' | 'fuel',
|
||||
rechargeDate: '',
|
||||
cardNo: '',
|
||||
gasStation: '',
|
||||
amount: null as number | null,
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = computed(() => ({
|
||||
licensePlate:
|
||||
dialogMode.value === 'supplement'
|
||||
? [{ required: true, message: '请选择车牌号', trigger: 'change' }]
|
||||
: [],
|
||||
rechargeType:
|
||||
dialogMode.value === 'supplement'
|
||||
? [{ required: true, message: '请选择充值类型', trigger: 'change' }]
|
||||
: [],
|
||||
rechargeDate: [{ required: true, message: '请选择充值日期', trigger: 'change' }],
|
||||
amount: [{ required: true, message: '请输入充值金额', trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewRow = ref<VehicleRechargeDisburseBoardItem | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const row = viewRow.value
|
||||
if (!row) return []
|
||||
const sections: ViewSection[] = [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: row.applyNo },
|
||||
{ label: '申请日期', value: row.applyDate },
|
||||
{
|
||||
label: '申请状态',
|
||||
value: getPlRechargeApplyStatusLabel(row.applyStatus),
|
||||
tagType: getPlRechargeApplyStatusTagType(row.applyStatus)
|
||||
},
|
||||
{ label: '车牌号', value: row.licensePlate },
|
||||
{
|
||||
label: '充值类型',
|
||||
value: getPlRechargeTypeLabel(row.rechargeType),
|
||||
tagType: getPlRechargeTypeTagType(row.rechargeType)
|
||||
},
|
||||
{ label: '申请人', value: row.applicantName || '—' },
|
||||
{
|
||||
label: '期初公里数',
|
||||
value: row.odometerKm != null ? `${Number(row.odometerKm).toFixed(1)} km` : '—'
|
||||
},
|
||||
{ label: '申请备注', value: row.applyRemark || '—', span: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
if (row.recordId) {
|
||||
sections.push({
|
||||
title: '发放信息',
|
||||
fields: [
|
||||
{ label: '充值订单号', value: row.rechargeOrderNo || '—' },
|
||||
{ label: '充值日期', value: row.rechargeDate || '—' },
|
||||
{ label: '卡号', value: row.cardNo || '—' },
|
||||
{ label: '加油站/渠道', value: row.gasStation || '—' },
|
||||
{
|
||||
label: '充值金额',
|
||||
value: row.amount != null ? `${Number(row.amount).toFixed(2)} 元` : '—'
|
||||
},
|
||||
{ label: '登记人', value: row.recordCreateByName || '—' },
|
||||
{ label: '登记备注', value: row.recordRemark || '—', span: 2 }
|
||||
]
|
||||
})
|
||||
}
|
||||
return sections
|
||||
})
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const vehicles = await vehicleApi.list()
|
||||
vehicleOptions.value = (vehicles.data ?? vehicles) || []
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleRechargeRecordApi.disburseBoard({
|
||||
q: searchForm.q || undefined,
|
||||
rechargeType: searchForm.rechargeType || undefined,
|
||||
applyStatus: searchForm.applyStatus || undefined,
|
||||
applyDateFrom: applyDateRange.value?.[0],
|
||||
applyDateTo: applyDateRange.value?.[1],
|
||||
rechargeDateFrom: rechargeDateRange.value?.[0],
|
||||
rechargeDateTo: rechargeDateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.rechargeType = ''
|
||||
searchForm.applyStatus = ''
|
||||
applyDateRange.value = []
|
||||
rechargeDateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.rechargeOrderNo = ''
|
||||
formData.applyId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.rechargeType = 'etc'
|
||||
formData.rechargeDate = ''
|
||||
formData.cardNo = ''
|
||||
formData.gasStation = ''
|
||||
formData.amount = null
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
currentApplyRow.value = null
|
||||
}
|
||||
|
||||
function handleDisburse(row: VehicleRechargeDisburseBoardItem) {
|
||||
resetForm()
|
||||
dialogMode.value = 'disburse'
|
||||
dialogTitle.value = '发放登记'
|
||||
currentApplyRow.value = row
|
||||
formData.applyId = row.applyId
|
||||
formData.licensePlate = row.licensePlate
|
||||
formData.rechargeType = row.rechargeType
|
||||
formData.rechargeDate = new Date().toISOString().slice(0, 10)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSupplement() {
|
||||
resetForm()
|
||||
dialogMode.value = 'supplement'
|
||||
dialogTitle.value = '无申请补录'
|
||||
formData.rechargeDate = new Date().toISOString().slice(0, 10)
|
||||
await loadVehicleOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleRechargeDisburseBoardItem) {
|
||||
if (!row.recordId) return
|
||||
resetForm()
|
||||
dialogMode.value = 'edit'
|
||||
dialogTitle.value = '编辑发放登记'
|
||||
currentApplyRow.value = row
|
||||
const res = await vehicleRechargeRecordApi.get(row.recordId)
|
||||
const detail = res.data ?? res
|
||||
Object.assign(formData, {
|
||||
id: detail.id,
|
||||
rechargeOrderNo: detail.rechargeOrderNo,
|
||||
applyId: detail.applyId || row.applyId,
|
||||
licensePlate: detail.licensePlate || row.licensePlate,
|
||||
rechargeType: detail.rechargeType,
|
||||
rechargeDate: detail.rechargeDate,
|
||||
cardNo: detail.cardNo || '',
|
||||
gasStation: detail.gasStation || '',
|
||||
amount: Number(detail.amount),
|
||||
attachmentUrls: detail.attachmentUrls || '',
|
||||
remark: detail.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleView(row: VehicleRechargeDisburseBoardItem) {
|
||||
viewRow.value = row
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applyId: formData.applyId || undefined,
|
||||
licensePlate: dialogMode.value === 'supplement' ? formData.licensePlate : undefined,
|
||||
rechargeType:
|
||||
dialogMode.value === 'supplement'
|
||||
? formData.rechargeType
|
||||
: dialogMode.value === 'edit'
|
||||
? formData.rechargeType
|
||||
: currentApplyRow.value?.rechargeType,
|
||||
rechargeDate: formData.rechargeDate,
|
||||
cardNo: formData.cardNo || undefined,
|
||||
gasStation: formData.gasStation || undefined,
|
||||
amount: formData.amount,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (dialogMode.value === 'edit') {
|
||||
await vehicleRechargeRecordApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleRechargeRecordApi.create(payload)
|
||||
ElMessage.success('发放登记成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleRechargeDisburseBoardItem) {
|
||||
if (!row.recordId) return
|
||||
await ElMessageBox.confirm('确定撤销该发放登记吗?关联申请将回退为待发放。', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.recordId] = true
|
||||
try {
|
||||
await vehicleRechargeRecordApi.delete(row.recordId)
|
||||
ElMessage.success('已撤销发放')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.recordId] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.apply-summary-block {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--primary-bg);
|
||||
}
|
||||
|
||||
.apply-summary-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.apply-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
min-width: 72px;
|
||||
margin-right: 6px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,523 @@
|
||||
<!-- 车辆尿素申请 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">车辆尿素申请</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="申请单号/车牌">
|
||||
<el-input v-model="searchForm.q" placeholder="搜索申请单号或车牌" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||||
<el-option label="待发放" value="pending" />
|
||||
<el-option label="已发放" value="disbursed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增申请
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
|
||||
<el-table-column prop="applyDate" label="申请时间" min-width="120" align="center" />
|
||||
<el-table-column prop="warehouseInDate" label="入库时间" min-width="120" align="center" />
|
||||
<el-table-column prop="unit" label="单位" min-width="90" align="center" />
|
||||
<el-table-column prop="quantity" label="入库数" min-width="100" align="right">
|
||||
<template #default="{ row }">{{ Number(row.quantity).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyAmount" label="申请金额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ Number(row.applyAmount).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="status" label="状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlUreaApplyStatusTagType(row.status)" size="small">
|
||||
{{ getPlUreaApplyStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<template v-if="row.status === 'pending'">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="warning" :loading="cancelLoadingMap[row.id]" @click="handleCancel(row)">
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button link type="danger" :loading="deleteLoadingMap[row.id]" @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item v-if="formData.applyNo" label="申请单号">
|
||||
<el-input v-model="formData.applyNo" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请人" prop="applicantId">
|
||||
<el-select
|
||||
v-model="formData.applicantId"
|
||||
filterable
|
||||
placeholder="请选择申请人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.displayName"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select v-model="formData.licensePlate" filterable placeholder="请选择车牌" style="width: 100%">
|
||||
<el-option v-for="item in vehicleOptions" :key="item.id" :label="item.licensePlate" :value="item.licensePlate" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请时间" prop="applyDate">
|
||||
<el-date-picker v-model="formData.applyDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="入库时间" prop="warehouseInDate">
|
||||
<el-date-picker v-model="formData.warehouseInDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位" prop="unit">
|
||||
<el-select
|
||||
v-model="formData.unit"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择或输入"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="item in PL_UREA_UNIT_PRESETS" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="入库数" prop="quantity">
|
||||
<el-input-number v-model="formData.quantity" :min="0.01" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请金额(元)" prop="applyAmount">
|
||||
<el-input-number v-model="formData.applyAmount" :min="0.01" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-urea/apply"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewDetail?.applyNo || '申请详情'"
|
||||
:sections="viewSections"
|
||||
:loading="viewLoading"
|
||||
width="720px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Check,
|
||||
CircleClose,
|
||||
Close,
|
||||
Delete,
|
||||
Edit,
|
||||
Plus,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleUreaApplyApi,
|
||||
type Vehicle,
|
||||
type VehicleUreaApply
|
||||
} from '@/api/pl'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import {
|
||||
getPlUreaApplyStatusLabel,
|
||||
getPlUreaApplyStatusTagType,
|
||||
PL_UREA_UNIT_PRESETS
|
||||
} from '@/utils/pl-display'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
})
|
||||
|
||||
const userOptions = ref<
|
||||
Array<{ userId: string; nickName: string; displayName: string }>
|
||||
>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const cancelLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleUreaApply[]>([])
|
||||
const selectedRows = ref<VehicleUreaApply[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const dateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
applyNo: '',
|
||||
applicantId: '',
|
||||
licensePlate: '',
|
||||
applyDate: '',
|
||||
warehouseInDate: '',
|
||||
unit: '吨桶',
|
||||
quantity: null as number | null,
|
||||
applyAmount: null as number | null,
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
applicantId: [{ required: true, message: '请选择申请人', trigger: 'change' }],
|
||||
licensePlate: [{ required: true, message: '请选择车牌号', trigger: 'change' }],
|
||||
applyDate: [{ required: true, message: '请选择申请时间', trigger: 'change' }],
|
||||
warehouseInDate: [{ required: true, message: '请选择入库时间', trigger: 'change' }],
|
||||
unit: [{ required: true, message: '请填写单位', trigger: 'change' }],
|
||||
quantity: [{ required: true, message: '请填写入库数', trigger: 'blur' }],
|
||||
applyAmount: [{ required: true, message: '请填写申请金额', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewLoading = ref(false)
|
||||
const viewDetail = ref<VehicleUreaApply | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDetail.value
|
||||
if (!d) return []
|
||||
return [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: d.applyNo },
|
||||
{ label: '车牌号', value: d.licensePlate },
|
||||
{ label: '申请时间', value: d.applyDate },
|
||||
{ label: '入库时间', value: d.warehouseInDate },
|
||||
{ label: '单位', value: d.unit },
|
||||
{ label: '入库数', value: Number(d.quantity).toFixed(2) },
|
||||
{ label: '申请金额', value: `${Number(d.applyAmount).toFixed(2)} 元` },
|
||||
{ label: '申请人', value: d.applicantName || '—' },
|
||||
{
|
||||
label: '状态',
|
||||
value: getPlUreaApplyStatusLabel(d.status),
|
||||
tagType: getPlUreaApplyStatusTagType(d.status)
|
||||
},
|
||||
{ label: '备注', value: d.remark || '—', span: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: unknown = (res as { data?: unknown })?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: Record<string, unknown>) => {
|
||||
const name = String(u.label || u.nickName || '')
|
||||
const account = String(u.userNo || '')
|
||||
return {
|
||||
userId: String(u.value ?? u.id ?? ''),
|
||||
nickName: name,
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const res = await vehicleApi.list()
|
||||
vehicleOptions.value = (res.data ?? res) || []
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleUreaApplyApi.list({
|
||||
q: searchForm.q || undefined,
|
||||
status: searchForm.status || undefined,
|
||||
applyDateFrom: dateRange.value?.[0],
|
||||
applyDateTo: dateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.status = ''
|
||||
dateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: VehicleUreaApply[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.applyNo = ''
|
||||
formData.applicantId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.applyDate = ''
|
||||
formData.warehouseInDate = ''
|
||||
formData.unit = '吨桶'
|
||||
formData.quantity = null
|
||||
formData.applyAmount = null
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
resetForm()
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增申请'
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
formData.applyDate = today
|
||||
formData.warehouseInDate = today
|
||||
formData.applicantId = currentUserId.value
|
||||
await loadUserOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleUreaApply) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑申请'
|
||||
await loadUserOptions()
|
||||
Object.assign(formData, {
|
||||
id: row.id,
|
||||
applyNo: row.applyNo,
|
||||
applicantId: row.applicantId || '',
|
||||
licensePlate: row.licensePlate,
|
||||
applyDate: row.applyDate,
|
||||
warehouseInDate: row.warehouseInDate,
|
||||
unit: row.unit || '吨桶',
|
||||
quantity: Number(row.quantity),
|
||||
applyAmount: Number(row.applyAmount),
|
||||
attachmentUrls: row.attachmentUrls || '',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleView(row: VehicleUreaApply) {
|
||||
viewVisible.value = true
|
||||
viewLoading.value = true
|
||||
try {
|
||||
const res = await vehicleUreaApplyApi.get(row.id)
|
||||
viewDetail.value = res.data ?? res
|
||||
} finally {
|
||||
viewLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (formData.applyDate && formData.warehouseInDate && formData.applyDate > formData.warehouseInDate) {
|
||||
ElMessage.warning('申请时间不能晚于入库时间')
|
||||
return
|
||||
}
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applicantId: formData.applicantId,
|
||||
licensePlate: formData.licensePlate,
|
||||
applyDate: formData.applyDate,
|
||||
warehouseInDate: formData.warehouseInDate,
|
||||
unit: formData.unit,
|
||||
quantity: formData.quantity,
|
||||
applyAmount: formData.applyAmount,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (isEdit.value) {
|
||||
await vehicleUreaApplyApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleUreaApplyApi.create(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel(row: VehicleUreaApply) {
|
||||
await ElMessageBox.confirm('确定作废该申请吗?', '提示', { type: 'warning' })
|
||||
cancelLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleUreaApplyApi.cancel(row.id)
|
||||
ElMessage.success('已作废')
|
||||
fetchData()
|
||||
} finally {
|
||||
cancelLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleUreaApply) {
|
||||
await ElMessageBox.confirm('确定删除该申请吗?', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.id] = true
|
||||
try {
|
||||
await vehicleUreaApplyApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.id] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadVehicleOptions(), loadUserOptions()])
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,573 @@
|
||||
<!-- 车辆尿素发放登记 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">车辆尿素发放登记</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="申请单号/车牌/发放单号">
|
||||
<el-input v-model="searchForm.q" placeholder="搜索关键词" clearable @keyup.enter="handleSearch" />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请状态">
|
||||
<el-select v-model="searchForm.applyStatus" placeholder="全部" clearable>
|
||||
<el-option label="待发放" value="pending" />
|
||||
<el-option label="已发放" value="disbursed" />
|
||||
<el-option label="已作废" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请日期">
|
||||
<el-date-picker
|
||||
v-model="applyDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="发放日期">
|
||||
<el-date-picker
|
||||
v-model="disburseDateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">共 <span class="sel-num">{{ pagination.total }}</span> 条申请</div>
|
||||
<el-button @click="handleSupplement">
|
||||
<el-icon><Plus /></el-icon>无申请补录
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="tableData" v-loading="loading" stripe row-key="applyId">
|
||||
<el-table-column prop="applyNo" label="申请单号" min-width="170" show-overflow-tooltip fixed="left" />
|
||||
<el-table-column prop="applyDate" label="申请时间" min-width="120" align="center" />
|
||||
<el-table-column prop="warehouseInDate" label="入库时间" min-width="120" align="center" />
|
||||
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" />
|
||||
<el-table-column prop="unit" label="单位" min-width="90" align="center" />
|
||||
<el-table-column prop="quantity" label="入库数" min-width="100" align="right">
|
||||
<template #default="{ row }">{{ Number(row.quantity).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyAmount" label="申请金额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">{{ Number(row.applyAmount).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="applyStatus" label="申请状态" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getPlUreaApplyStatusTagType(row.applyStatus)" size="small">
|
||||
{{ getPlUreaApplyStatusLabel(row.applyStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="licensePlate" label="车牌号" min-width="110" align="center" />
|
||||
<el-table-column prop="disburseNo" label="发放单号" min-width="170" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.disburseNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="disburseDate" label="发放时间" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.disburseDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="发放金额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.amount != null ? Number(row.amount).toFixed(2) : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="handlerName" label="处理人" min-width="100" align="center">
|
||||
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="300" fixed="right" align="center" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.applyStatus === 'pending'"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDisburse(row)"
|
||||
>
|
||||
<el-icon><Money /></el-icon>发放
|
||||
</el-button>
|
||||
<template v-if="row.applyStatus === 'disbursed' && row.disburseId">
|
||||
<el-button link type="primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
:loading="deleteLoadingMap[row.disburseId]"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>撤销发放
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" scroll-to-error>
|
||||
<div v-if="dialogMode === 'disburse' && currentApplyRow" class="apply-summary-block">
|
||||
<div class="apply-summary-title">关联申请</div>
|
||||
<div class="apply-summary-grid">
|
||||
<div><span class="label">申请单号</span>{{ currentApplyRow.applyNo }}</div>
|
||||
<div><span class="label">申请时间</span>{{ currentApplyRow.applyDate }}</div>
|
||||
<div><span class="label">车牌号</span>{{ currentApplyRow.licensePlate }}</div>
|
||||
<div><span class="label">入库时间</span>{{ currentApplyRow.warehouseInDate }}</div>
|
||||
<div><span class="label">单位</span>{{ currentApplyRow.unit }}</div>
|
||||
<div><span class="label">入库数</span>{{ Number(currentApplyRow.quantity).toFixed(2) }}</div>
|
||||
<div><span class="label">申请人</span>{{ currentApplyRow.applicantName || '—' }}</div>
|
||||
<div>
|
||||
<span class="label">申请金额</span>
|
||||
{{ Number(currentApplyRow.applyAmount).toFixed(2) }} 元
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="发放单号">
|
||||
<el-input :model-value="formData.disburseNo || '保存后自动生成'" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="处理人" prop="handlerId">
|
||||
<el-select
|
||||
v-model="formData.handlerId"
|
||||
filterable
|
||||
placeholder="请选择处理人"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userOptions"
|
||||
:key="user.userId"
|
||||
:label="user.displayName"
|
||||
:value="user.userId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<template v-if="dialogMode === 'supplement'">
|
||||
<el-form-item label="车牌号" prop="licensePlate">
|
||||
<el-select
|
||||
v-model="formData.licensePlate"
|
||||
filterable
|
||||
placeholder="请选择车牌"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in vehicleOptions"
|
||||
:key="item.id"
|
||||
:label="item.licensePlate"
|
||||
:value="item.licensePlate"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="发放时间" prop="disburseDate">
|
||||
<el-date-picker v-model="formData.disburseDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="发放金额(元)" prop="amount">
|
||||
<el-input-number v-model="formData.amount" :min="0.01" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件" class="form-col-span-2">
|
||||
<AttachmentUploadField
|
||||
ref="attachmentUploadRef"
|
||||
v-model="formData.attachmentUrls"
|
||||
folder="pl/vehicle-urea/disburse"
|
||||
accept="image/*,.pdf"
|
||||
:limit="5"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="form-col-span-2">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewVisible"
|
||||
:title="viewRow?.applyNo || '发放详情'"
|
||||
:sections="viewSections"
|
||||
width="760px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Check, Close, Delete, Edit, Money, Plus, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
||||
import {
|
||||
vehicleApi,
|
||||
vehicleUreaDisburseApi,
|
||||
type Vehicle,
|
||||
type VehicleUreaDisburseBoardItem
|
||||
} from '@/api/pl'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { getPlUreaApplyStatusLabel, getPlUreaApplyStatusTagType } from '@/utils/pl-display'
|
||||
|
||||
type DialogMode = 'disburse' | 'edit' | 'supplement'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const currentUserId = computed(() => {
|
||||
const info = userStore.info as Record<string, unknown> | undefined
|
||||
return String(info?.id || info?.userId || '')
|
||||
})
|
||||
|
||||
const userOptions = ref<
|
||||
Array<{ userId: string; nickName: string; displayName: string }>
|
||||
>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const deleteLoadingMap = reactive<Record<string, boolean>>({})
|
||||
const tableData = ref<VehicleUreaDisburseBoardItem[]>([])
|
||||
const vehicleOptions = ref<Vehicle[]>([])
|
||||
const applyDateRange = ref<string[]>([])
|
||||
const disburseDateRange = ref<string[]>([])
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
applyStatus: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const dialogMode = ref<DialogMode>('disburse')
|
||||
const currentApplyRow = ref<VehicleUreaDisburseBoardItem | null>(null)
|
||||
const formRef = ref()
|
||||
const attachmentUploadRef = ref<InstanceType<typeof AttachmentUploadField>>()
|
||||
|
||||
const formData = reactive({
|
||||
id: '',
|
||||
disburseNo: '',
|
||||
applyId: '',
|
||||
licensePlate: '',
|
||||
handlerId: '',
|
||||
disburseDate: '',
|
||||
amount: null as number | null,
|
||||
attachmentUrls: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = computed(() => ({
|
||||
licensePlate:
|
||||
dialogMode.value === 'supplement'
|
||||
? [{ required: true, message: '请选择车牌号', trigger: 'change' }]
|
||||
: [],
|
||||
handlerId: [{ required: true, message: '请选择处理人', trigger: 'change' }],
|
||||
disburseDate: [{ required: true, message: '请选择发放时间', trigger: 'change' }],
|
||||
amount: [{ required: true, message: '请输入发放金额', trigger: 'blur' }]
|
||||
}))
|
||||
|
||||
const viewVisible = ref(false)
|
||||
const viewRow = ref<VehicleUreaDisburseBoardItem | null>(null)
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
const row = viewRow.value
|
||||
if (!row) return []
|
||||
const sections: ViewSection[] = [
|
||||
{
|
||||
title: '申请信息',
|
||||
fields: [
|
||||
{ label: '申请单号', value: row.applyNo },
|
||||
{ label: '申请时间', value: row.applyDate },
|
||||
{ label: '入库时间', value: row.warehouseInDate },
|
||||
{
|
||||
label: '申请状态',
|
||||
value: getPlUreaApplyStatusLabel(row.applyStatus),
|
||||
tagType: getPlUreaApplyStatusTagType(row.applyStatus)
|
||||
},
|
||||
{ label: '车牌号', value: row.licensePlate },
|
||||
{ label: '单位', value: row.unit },
|
||||
{ label: '入库数', value: Number(row.quantity).toFixed(2) },
|
||||
{ label: '申请金额', value: `${Number(row.applyAmount).toFixed(2)} 元` },
|
||||
{ label: '申请人', value: row.applicantName || '—' },
|
||||
{ label: '申请备注', value: row.applyRemark || '—', span: 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
if (row.disburseId) {
|
||||
sections.push({
|
||||
title: '发放信息',
|
||||
fields: [
|
||||
{ label: '发放单号', value: row.disburseNo || '—' },
|
||||
{ label: '发放时间', value: row.disburseDate || '—' },
|
||||
{
|
||||
label: '发放金额',
|
||||
value: row.amount != null ? `${Number(row.amount).toFixed(2)} 元` : '—'
|
||||
},
|
||||
{ label: '处理人', value: row.handlerName || '—' },
|
||||
{ label: '发放备注', value: row.disburseRemark || '—', span: 2 }
|
||||
]
|
||||
})
|
||||
}
|
||||
return sections
|
||||
})
|
||||
|
||||
async function loadUserOptions() {
|
||||
try {
|
||||
const res = await fetchUserOptions()
|
||||
const data: unknown = (res as { data?: unknown })?.data ?? res ?? []
|
||||
userOptions.value = (Array.isArray(data) ? data : []).map((u: Record<string, unknown>) => {
|
||||
const name = String(u.label || u.nickName || '')
|
||||
const account = String(u.userNo || '')
|
||||
return {
|
||||
userId: String(u.value ?? u.id ?? ''),
|
||||
nickName: name,
|
||||
displayName: account ? `${name}(${account})` : name
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
userOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadVehicleOptions() {
|
||||
const vehicles = await vehicleApi.list()
|
||||
vehicleOptions.value = (vehicles.data ?? vehicles) || []
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await vehicleUreaDisburseApi.disburseBoard({
|
||||
q: searchForm.q || undefined,
|
||||
applyStatus: searchForm.applyStatus || undefined,
|
||||
applyDateFrom: applyDateRange.value?.[0],
|
||||
applyDateTo: applyDateRange.value?.[1],
|
||||
disburseDateFrom: disburseDateRange.value?.[0],
|
||||
disburseDateTo: disburseDateRange.value?.[1],
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize
|
||||
})
|
||||
const data = res.data ?? res
|
||||
tableData.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.q = ''
|
||||
searchForm.applyStatus = ''
|
||||
applyDateRange.value = []
|
||||
disburseDateRange.value = []
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.id = ''
|
||||
formData.disburseNo = ''
|
||||
formData.applyId = ''
|
||||
formData.licensePlate = ''
|
||||
formData.handlerId = ''
|
||||
formData.disburseDate = ''
|
||||
formData.amount = null
|
||||
formData.attachmentUrls = ''
|
||||
formData.remark = ''
|
||||
currentApplyRow.value = null
|
||||
}
|
||||
|
||||
async function handleDisburse(row: VehicleUreaDisburseBoardItem) {
|
||||
resetForm()
|
||||
dialogMode.value = 'disburse'
|
||||
dialogTitle.value = '发放登记'
|
||||
currentApplyRow.value = row
|
||||
formData.applyId = row.applyId
|
||||
formData.licensePlate = row.licensePlate
|
||||
formData.amount = Number(row.applyAmount)
|
||||
formData.disburseDate = new Date().toISOString().slice(0, 10)
|
||||
formData.handlerId = currentUserId.value
|
||||
await loadUserOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSupplement() {
|
||||
resetForm()
|
||||
dialogMode.value = 'supplement'
|
||||
dialogTitle.value = '无申请补录'
|
||||
formData.disburseDate = new Date().toISOString().slice(0, 10)
|
||||
formData.handlerId = currentUserId.value
|
||||
await Promise.all([loadVehicleOptions(), loadUserOptions()])
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleEdit(row: VehicleUreaDisburseBoardItem) {
|
||||
if (!row.disburseId) return
|
||||
resetForm()
|
||||
dialogMode.value = 'edit'
|
||||
dialogTitle.value = '编辑发放登记'
|
||||
currentApplyRow.value = row
|
||||
await loadUserOptions()
|
||||
const res = await vehicleUreaDisburseApi.get(row.disburseId)
|
||||
const detail = res.data ?? res
|
||||
Object.assign(formData, {
|
||||
id: detail.id,
|
||||
disburseNo: detail.disburseNo,
|
||||
applyId: detail.applyId || row.applyId,
|
||||
licensePlate: detail.licensePlate || row.licensePlate,
|
||||
handlerId: detail.handlerId,
|
||||
disburseDate: detail.disburseDate,
|
||||
amount: Number(detail.amount),
|
||||
attachmentUrls: detail.attachmentUrls || '',
|
||||
remark: detail.remark || ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleView(row: VehicleUreaDisburseBoardItem) {
|
||||
viewRow.value = row
|
||||
viewVisible.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await formRef.value?.validate()
|
||||
if (attachmentUploadRef.value?.hasUploading?.()) {
|
||||
ElMessage.warning('附件上传中,请稍候')
|
||||
return
|
||||
}
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
applyId: formData.applyId || undefined,
|
||||
licensePlate: dialogMode.value === 'supplement' ? formData.licensePlate : undefined,
|
||||
handlerId: formData.handlerId,
|
||||
disburseDate: formData.disburseDate,
|
||||
amount: formData.amount,
|
||||
attachmentUrls: formData.attachmentUrls || undefined,
|
||||
remark: formData.remark || undefined
|
||||
}
|
||||
if (dialogMode.value === 'edit') {
|
||||
await vehicleUreaDisburseApi.update(formData.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await vehicleUreaDisburseApi.create(payload)
|
||||
ElMessage.success('发放登记成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: VehicleUreaDisburseBoardItem) {
|
||||
if (!row.disburseId) return
|
||||
await ElMessageBox.confirm('确定撤销该发放登记吗?关联申请将回退为待发放。', '提示', { type: 'warning' })
|
||||
deleteLoadingMap[row.disburseId] = true
|
||||
try {
|
||||
await vehicleUreaDisburseApi.delete(row.disburseId)
|
||||
ElMessage.success('已撤销发放')
|
||||
fetchData()
|
||||
} finally {
|
||||
deleteLoadingMap[row.disburseId] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.apply-summary-block {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--primary-bg);
|
||||
}
|
||||
|
||||
.apply-summary-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.apply-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px 16px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
min-width: 72px;
|
||||
margin-right: 6px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -126,7 +126,7 @@
|
||||
<el-button link type="primary" @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button link type="danger" @click.stop="handleDelete(row)">
|
||||
<el-button link type="danger" :disabled="!canDeleteByCreator(row)" :title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP" @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -167,7 +167,7 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.vehicleType === 'carrier'" label="承运商" prop="carrierId">
|
||||
<el-form-item label="承运商" prop="carrierId">
|
||||
<el-select v-model="formData.carrierId" placeholder="请选择承运商" clearable filterable>
|
||||
<el-option
|
||||
v-for="c in carrierList"
|
||||
@@ -259,10 +259,12 @@
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 车辆证书(卡片) -->
|
||||
<PlCertFormList
|
||||
<!-- 车辆证书(主车/挂车 Tab) -->
|
||||
<PlVehicleCertPartTabs
|
||||
ref="certFormListRef"
|
||||
v-model="certList"
|
||||
:license-plate="formData.licensePlate"
|
||||
:trailer-no="formData.trailerNo"
|
||||
title="车辆证书"
|
||||
upload-folder="pl/vehicle-cert"
|
||||
:cert-name-options="certNameOptions"
|
||||
@@ -294,6 +296,8 @@
|
||||
<template #vehicleDetailTabs>
|
||||
<PlVehicleCertPersonnelTabs
|
||||
:cert-list="viewDetail?.certList || []"
|
||||
:license-plate="viewDetail?.licensePlate"
|
||||
:trailer-no="viewDetail?.trailerNo"
|
||||
:personnel-list="viewPersonnelList"
|
||||
/>
|
||||
</template>
|
||||
@@ -314,7 +318,7 @@ import {
|
||||
type Carrier,
|
||||
type Personnel
|
||||
} from '@/api/pl'
|
||||
import PlCertFormList from '@/components/business/pl-cert-form-list/index.vue'
|
||||
import PlVehicleCertPartTabs from '@/components/business/pl-vehicle-cert-part-tabs/index.vue'
|
||||
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import SystemViewDialog, {
|
||||
@@ -322,9 +326,15 @@ import SystemViewDialog, {
|
||||
} from '@/components/business/system-view-dialog/index.vue'
|
||||
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import {
|
||||
PL_VEHICLE_CERT_NAME_OPTIONS,
|
||||
splitVehicleCertsByPart,
|
||||
validateVehicleRequiredCerts
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
|
||||
type CertListItem = CertRow & { _key?: string }
|
||||
type CertFormListRef = InstanceType<typeof PlCertFormList>
|
||||
type CertFormListRef = InstanceType<typeof PlVehicleCertPartTabs>
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
@@ -346,14 +356,7 @@ const viewLoading = ref(false)
|
||||
const viewDetail = ref<VehicleDetail | null>(null)
|
||||
const viewPersonnelList = ref<VehiclePersonnelViewRow[]>([])
|
||||
|
||||
const certNameOptions = [
|
||||
'行驶证',
|
||||
'道路运输证',
|
||||
'驾驶证',
|
||||
'押运员证',
|
||||
'危化品从业资格证',
|
||||
'保险单'
|
||||
]
|
||||
const certNameOptions = PL_VEHICLE_CERT_NAME_OPTIONS
|
||||
|
||||
const searchForm = reactive({
|
||||
q: '',
|
||||
@@ -597,13 +600,17 @@ function validateSubTables(): boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const cert of certList.value) {
|
||||
if (!cert.certName?.trim()) {
|
||||
ElMessage.warning('证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
const hasTrailer = Boolean(formData.trailerNo?.trim())
|
||||
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList.value)
|
||||
return validateVehicleRequiredCerts({
|
||||
headCerts,
|
||||
trailerCerts,
|
||||
hasTrailer,
|
||||
licensePlate: formData.licensePlate,
|
||||
trailerNo: formData.trailerNo,
|
||||
onError: message => ElMessage.warning(message)
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
@@ -720,6 +727,7 @@ async function handleEdit(row: Vehicle) {
|
||||
certList.value = (detail.certList || []).map(c => ({
|
||||
_key: c.id || c.certId || `cert-${Math.random().toString(36).slice(2, 10)}`,
|
||||
certId: c.id || c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName,
|
||||
certNo: c.certNo,
|
||||
expireDate: c.expireDate,
|
||||
@@ -769,6 +777,7 @@ async function handleSubmit() {
|
||||
})),
|
||||
certList: certList.value.map(c => ({
|
||||
certId: c.certId,
|
||||
vehiclePart: c.vehiclePart,
|
||||
certName: c.certName.trim(),
|
||||
certNo: c.certNo || undefined,
|
||||
expireDate: c.expireDate || undefined,
|
||||
@@ -796,6 +805,10 @@ function handleDialogClose() {
|
||||
}
|
||||
|
||||
async function handleDelete(row: Vehicle) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm('确定删除该车辆吗?', '提示', { type: 'warning' })
|
||||
await vehicleApi.delete(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -256,6 +256,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -702,7 +703,12 @@ const dictStore = useDictStore()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Dept.DeptItem) {
|
||||
async function handleDelete(row: Api.Dept.DeptItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
try {
|
||||
const childCount = row.children?.length || 0
|
||||
const confirmText = childCount
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
@@ -118,7 +119,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -346,7 +347,14 @@
|
||||
<el-button class="btn-op-item primary" @click="handleEditDictData(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDeleteDictData(row)"> 删除 </el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDeleteDictData(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -449,6 +457,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -635,7 +644,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Dict.DictTypeItem) {
|
||||
async function handleDelete(row: Api.Dict.DictTypeItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
await ElMessageBox.confirm(`确定删除字典类型「${row.dictName}」吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
@@ -644,7 +658,16 @@
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map((r) => r.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请选择要删除的字典类型')
|
||||
return
|
||||
@@ -767,6 +790,10 @@
|
||||
}
|
||||
|
||||
async function handleDeleteDictData(row: Api.Dict.DictDataItem) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除字典数据「${row.dictLabel}」吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
@@ -123,7 +123,12 @@
|
||||
<el-button type="success" link size="small" @click="handleSubmit(row.id)"
|
||||
>提交审批</el-button
|
||||
>
|
||||
<el-button type="danger" link size="small" @click="handleDelete(row.id)"
|
||||
<el-button
|
||||
v-if="canDeleteByCreator(row)"
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click="handleDelete(row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
@@ -237,6 +242,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Close, Check, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
fetchLeaveList,
|
||||
fetchCreateLeave,
|
||||
@@ -365,10 +371,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
async function handleDelete(row: { id: string }) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm('确定删除此请假申请?', '提示', { type: 'warning' })
|
||||
await fetchDeleteLeave(id)
|
||||
await fetchDeleteLeave(row.id)
|
||||
loadData()
|
||||
} catch {
|
||||
// cancelled or error
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -267,6 +267,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -536,7 +537,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Menu.MenuItem) {
|
||||
async function handleDelete(row: Api.Menu.MenuItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
try {
|
||||
const childCount = row.children?.length || 0
|
||||
const confirmText = childCount
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
<!-- 操作日志 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">操作日志</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="操作模块">
|
||||
<el-input v-model="searchForm.title" placeholder="模块名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-select v-model="searchForm.businessType" placeholder="全部" clearable>
|
||||
<el-option
|
||||
v-for="opt in OPER_LOG_BUSINESS_TYPE_OPTIONS"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人员">
|
||||
<el-input v-model="searchForm.operName" placeholder="姓名或账号" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.status" placeholder="全部" clearable>
|
||||
<el-option label="成功" :value="0" />
|
||||
<el-option label="失败" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按模块、类型、操作人筛选操作日志。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="tableData" stripe v-loading="loading" row-key="id">
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="title" label="操作模块" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="操作类型" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getOperLogBusinessTypeTagType(row.businessType)" size="small">
|
||||
{{ getOperLogBusinessTypeLabel(row.businessType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="operSummary"
|
||||
label="操作摘要"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="operName" label="操作人员" min-width="120" align="center" />
|
||||
<el-table-column prop="operTime" label="操作时间" min-width="170" align="center" />
|
||||
<el-table-column label="状态" min-width="90" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 0 ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === 0 ? '成功' : '失败' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="costTime" label="耗时(ms)" min-width="100" align="center" />
|
||||
<el-table-column
|
||||
label="操作"
|
||||
fixed="right"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="openDetail(row)">
|
||||
<el-icon><View /></el-icon>详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="fetchList"
|
||||
@current-change="fetchList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="detailVisible"
|
||||
title="操作日志详情"
|
||||
width="720px"
|
||||
align-center
|
||||
class="dialog-form-layout operlog-detail-dialog"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-if="detailRow" class="operlog-detail-body">
|
||||
<div class="detail-kv-grid">
|
||||
<div class="detail-kv-item"><span class="label">操作模块</span><span>{{ detailRow.title }}</span></div>
|
||||
<div class="detail-kv-item">
|
||||
<span class="label">操作类型</span>
|
||||
<el-tag :type="getOperLogBusinessTypeTagType(detailRow.businessType)" size="small">
|
||||
{{ getOperLogBusinessTypeLabel(detailRow.businessType) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-kv-item"><span class="label">操作人员</span><span>{{ detailRow.operName || '—' }}</span></div>
|
||||
<div class="detail-kv-item"><span class="label">操作时间</span><span>{{ detailRow.operTime || '—' }}</span></div>
|
||||
<div class="detail-kv-item full"><span class="label">操作摘要</span><span>{{ detailRow.operSummary || '—' }}</span></div>
|
||||
<div class="detail-kv-item full"><span class="label">请求地址</span><span>{{ detailRow.operUrl || '—' }}</span></div>
|
||||
<div class="detail-kv-item"><span class="label">操作 IP</span><span>{{ detailRow.operIp || '—' }}</span></div>
|
||||
<div class="detail-kv-item"><span class="label">耗时</span><span>{{ detailRow.costTime ?? '—' }} ms</span></div>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<div class="detail-block-title">请求参数</div>
|
||||
<pre class="detail-pre">{{ detailRow.operParam || '无' }}</pre>
|
||||
</div>
|
||||
<div class="detail-block">
|
||||
<div class="detail-block-title">返回参数</div>
|
||||
<pre class="detail-pre">{{ detailRow.jsonResult || '无' }}</pre>
|
||||
</div>
|
||||
<div v-if="detailRow.status === 1" class="detail-block">
|
||||
<div class="detail-block-title">错误信息</div>
|
||||
<pre class="detail-pre error">{{ detailRow.errorMsg || '—' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
||||
import { list as listOperLog, detail as getOperLogDetail } from '@/api/monitor/operlog'
|
||||
import type { OperLogVO, OperLogQuery } from '@/api/monitor/operlog/types'
|
||||
import {
|
||||
OPER_LOG_BUSINESS_TYPE_OPTIONS,
|
||||
getOperLogBusinessTypeLabel,
|
||||
getOperLogBusinessTypeTagType
|
||||
} from '@/api/monitor/operlog/types'
|
||||
|
||||
const loading = ref(false)
|
||||
const tableData = ref<OperLogVO[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const searchForm = reactive({
|
||||
title: '',
|
||||
businessType: undefined as number | undefined,
|
||||
operName: '',
|
||||
status: undefined as number | undefined
|
||||
})
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailRow = ref<OperLogVO | null>(null)
|
||||
|
||||
function buildQuery(): OperLogQuery {
|
||||
return {
|
||||
pageNum: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
title: searchForm.title || undefined,
|
||||
businessType: searchForm.businessType,
|
||||
operName: searchForm.operName || undefined,
|
||||
status: searchForm.status
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listOperLog(buildQuery())
|
||||
const payload = res as { rows?: OperLogVO[]; list?: OperLogVO[]; total?: number }
|
||||
tableData.value = payload?.rows || payload?.list || []
|
||||
pagination.total = payload?.total ?? tableData.value.length
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.title = ''
|
||||
searchForm.businessType = undefined
|
||||
searchForm.operName = ''
|
||||
searchForm.status = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
async function openDetail(row: OperLogVO) {
|
||||
try {
|
||||
const res = await getOperLogDetail({ id: String(row.id) })
|
||||
detailRow.value = (res as OperLogVO) || row
|
||||
} catch {
|
||||
detailRow.value = row
|
||||
}
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-kv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-kv-item {
|
||||
font-size: 14px;
|
||||
color: var(--text, #1e293b);
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #64748b);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-block {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.detail-block-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.detail-pre {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
|
||||
&.error {
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -40,7 +40,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
@@ -126,7 +127,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -317,6 +318,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -708,7 +710,12 @@ const dictStore = useDictStore()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Post.PostItem) {
|
||||
async function handleDelete(row: Api.Post.PostItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
await ElMessageBox.confirm(`确定删除职位「${row.postName}」吗?`, '提示', { type: 'warning' })
|
||||
await fetchDeletePost(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
@@ -716,7 +723,16 @@ const dictStore = useDictStore()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map((r) => r.id)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个职位吗?`, '提示', {
|
||||
type: 'warning'
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
@@ -132,7 +133,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -422,6 +423,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -753,14 +755,28 @@ const dictStore = useDictStore()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Role.RoleItem) {
|
||||
async function handleDelete(row: Api.Role.RoleItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
await ElMessageBox.confirm(`确定删除角色「${row.roleName}」吗?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteRole(row.id)
|
||||
await loadData()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map((r) => r.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请至少选择一项')
|
||||
return
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
@@ -155,7 +156,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -299,6 +300,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check, View, Edit, Delete, More, Location, RefreshLeft, Search } from '@element-plus/icons-vue'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
@@ -648,7 +650,12 @@ const dictStore = useDictStore()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Site.SiteItem) {
|
||||
async function handleDelete(row: Api.Site.SiteItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
await ElMessageBox.confirm(`确定删除站点「${row.siteName}」吗?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteSite(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
@@ -656,7 +663,16 @@ const dictStore = useDictStore()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map((r) => r.id)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个站点吗?`, '提示', {
|
||||
type: 'warning'
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="deletableSelection.length === 0"
|
||||
:disabled="deletableSelection.length === 0 || !allRowsDeletableByCreator(deletableSelection)"
|
||||
:title="allRowsDeletableByCreator(deletableSelection) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除
|
||||
@@ -141,7 +142,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided :disabled="isDefaultTenantRow(row)">
|
||||
<el-dropdown-item command="delete" divided :disabled="isDefaultTenantRow(row) || !canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -229,6 +230,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -424,6 +426,10 @@
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Tenant.TenantItem) {
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (isDefaultTenantRow(row)) {
|
||||
ElMessage.warning('默认租户不可删除')
|
||||
return
|
||||
@@ -434,7 +440,16 @@
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = deletableSelection.value.map((r) => r.id)
|
||||
const rows = filterDeletableByCreator(deletableSelection.value)
|
||||
if (!rows.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (rows.length < deletableSelection.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = rows.map((r) => r.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('所选中含默认租户或无可删项,请重新选择')
|
||||
return
|
||||
|
||||
@@ -93,7 +93,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -294,6 +295,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
@@ -569,7 +571,16 @@ const dictStore = useDictStore()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const rows = selectedRows.value
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const rows = deletable
|
||||
if (!rows.length) {
|
||||
ElMessage.warning('请至少选择一项')
|
||||
return
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
@@ -129,7 +130,7 @@
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -208,6 +209,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
Plus,
|
||||
Close,
|
||||
Check,
|
||||
@@ -358,14 +360,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Workflow.ProcessDefItem) {
|
||||
async function handleDelete(row: Api.Workflow.ProcessDefItem)
|
||||
if (!canDeleteByCreator(row)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
{
|
||||
await ElMessageBox.confirm('确定要删除该流程吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteProcessDef(row.id)
|
||||
await loadData()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
const deletable = filterDeletableByCreator(selectedRows.value)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < selectedRows.value.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const ids = deletable.map((r) => r.id)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个流程吗?`, '提示', {
|
||||
type: 'warning'
|
||||
|
||||
Reference in new issue
Block a user