This commit is contained in:
zlwu committed 2026-07-05 10:07:24 +08:00
1 parent 33c038b8d9
commit 54b27a3f0e
253 files changed
+30060 -14298

No files matched your search

+118 -175
View File
@@ -2,13 +2,6 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<div class="stats">
<div v-for="item in stats" :key="item.name" class="stat" :class="{ 'stat-money': item.name.includes('金额') }">
<p class="k">{{ item.name }}</p>
<p class="v">{{ item.value }}</p>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">备用金申请列表</h2>
@@ -42,35 +35,17 @@
style="width: 100%"
/>
</el-form-item>
</div>
<div v-show="showMoreFilters" id="advance-apply-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="当前节点">
<el-select v-model="filterForm.currentNode" placeholder="全部" clearable>
<el-option label="草稿" value="草稿" />
<el-option label="发起人主管审批" value="发起人主管审批" />
<el-option label="财务复核" value="财务复核" />
<el-option label="总经理审批" value="总经理审批" />
</el-select>
</el-form-item>
</div>
<el-form-item label="当前节点">
<el-select v-model="filterForm.currentNode" placeholder="全部" clearable>
<el-option label="草稿" value="草稿" />
<el-option label="发起人主管审批" value="发起人主管审批" />
<el-option label="财务复核" value="财务复核" />
<el-option label="总经理审批" value="总经理审批" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="showMoreFilters"
aria-controls="advance-apply-filter-extra"
@click="toggleMoreFilters"
>
{{ showMoreFilters ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="showMoreFilters" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -88,6 +63,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="申请单号/申请原因"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="openAdd">
<el-icon><Plus /></el-icon>新增备用金申请
</el-button>
@@ -117,13 +98,21 @@
<el-button class="btn-ghost" @click="openFlowDialog()">
<el-icon><View /></el-icon>查看审批流
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="handleSearch"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<div class="table-scroll table-page-summary advance-apply-summary-merge">
<div class="table-scroll table-page-summary">
<el-table
class="list-table"
:data="rows"
@@ -138,6 +127,7 @@
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -148,6 +138,7 @@
<el-table-column
prop="applyCode"
label="申请单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -158,6 +149,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
@@ -165,6 +157,7 @@
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('reason')"
prop="reason"
label="申请原因"
min-width="160"
@@ -173,6 +166,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="申请金额(元)"
:formatter="moneyTableFormatter"
@@ -182,6 +176,8 @@
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('actualAmount')"
prop="actualAmount"
label="实际发放金额(元)"
class-name="col-money"
min-width="130"
@@ -197,6 +193,8 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('offsetAmount')"
prop="offsetAmount"
label="冲抵总金额(元)"
class-name="col-money"
min-width="130"
@@ -207,14 +205,28 @@
{{ formatMoney(row.offsetAmount || 0) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="110" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('status')"
prop="status"
label="状态"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">
{{ statusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="currentNode" label="当前节点" min-width="120" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('currentNode')"
prop="currentNode"
label="当前节点"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="shouldShowWorkflowNodeTag(row.currentNode, row.status)"
@@ -257,7 +269,9 @@
ref="addFormRef"
:model="addForm"
:rules="addFormRules"
label-position="top"
label-position="right"
label-width="100px"
class="bx-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -379,25 +393,12 @@
</el-table>
</BaseDetailTableSection>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="viewFlowNodes" :loading="viewFlowLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="viewApprovalHistory" :loading="viewFlowLoading" />
</div>
</div>
<BxApprovalFlowTabs
:nodes="viewFlowNodes"
:history="viewApprovalHistory"
:loading="viewFlowLoading"
/>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.view = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
<el-dialog
@@ -408,30 +409,16 @@
align-center
destroy-on-close
>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="flowNodes" :loading="flowLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="flowHistory" :loading="flowLoading" />
</div>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.viewFlow = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
<BxApprovalFlowTabs :nodes="flowNodes" :history="flowHistory" :loading="flowLoading" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { reactive, ref, computed, onMounted, nextTick, watch } from 'vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { reactive, ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance } from 'element-plus'
import {
@@ -443,16 +430,13 @@ import {
Delete,
Check,
Search,
RefreshLeft,
ArrowUpBold,
ArrowDownBold
RefreshLeft
} from '@element-plus/icons-vue'
import { advanceApi, paymentApi } from '@/api/bx'
import { canDeleteByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import {
statusLabel,
statusTagType,
formatSumMoney,
formatMoney,
formatMoneyYuan,
moneyTableFormatter,
@@ -462,16 +446,15 @@ import {
shouldShowWorkflowNodeTag
} from '@/utils/bx/format'
import { useServerPaged } from '@/hooks/bx/useServerPaged'
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
import ApprovalHistoryView from '@/components/bx/base/ApprovalHistoryView.vue'
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
import BaseDetailTableSection from '@/components/bx/base/BaseDetailTableSection.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import { bxViewKv, bxViewKvLong, bxViewKvMoney } from '@/utils/bx/view-kv'
import { bxViewKv, bxViewKvMoney } from '@/utils/bx/view-kv'
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
type DialogKey = 'add' | 'view' | 'viewFlow'
const showMoreFilters = ref(false)
const loadingMap = reactive<Record<string, boolean>>({})
const dialogs = reactive<Record<DialogKey, boolean>>({
add: false,
@@ -485,40 +468,26 @@ const { rows, total, currentPage, pageSize, rangeText, loading, refresh, search,
return res.data
})
const myAdvanceStats = ref({
pendingCount: 0,
monthApplyAmount: 0,
issuedAmount: 0,
offsetTotal: 0,
pendingIssueCount: 0
})
const quickSearchKeyword = ref('')
async function loadMyAdvanceStats() {
try {
const res: any = await advanceApi.myStats()
const d = res.data || {}
myAdvanceStats.value = {
pendingCount: Number(d.pendingCount ?? 0),
monthApplyAmount: Number(d.monthApplyAmount ?? 0),
issuedAmount: Number(d.issuedAmount ?? 0),
offsetTotal: Number(d.offsetTotal ?? 0),
pendingIssueCount: Number(d.pendingIssueCount ?? 0)
}
} catch {
/* 拦截器已提示 */
}
}
const listColumnDefs = [
{ prop: 'applyTime', label: '申请时间' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'reason', label: '申请原因' },
{ prop: 'amount', label: '申请金额(元)' },
{ prop: 'actualAmount', label: '实际发放金额(元)' },
{ prop: 'offsetAmount', label: '冲抵总金额(元)', defaultVisible: false },
{ prop: 'status', label: '状态' },
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
]
const stats = computed(() => {
const s = myAdvanceStats.value
return [
{ name: '待审批申请', value: String(s.pendingCount) },
{ name: '本月申请金额(元)', value: formatMoney(s.monthApplyAmount) },
{ name: '已发放金额(元)', value: formatMoney(s.issuedAmount) },
{ name: '累计已冲抵金额(元)', value: formatMoney(s.offsetTotal) },
{ name: '待发放申请', value: String(s.pendingIssueCount) }
]
})
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('bx-advance-apply-list-columns', listColumnDefs)
const filterForm = reactive({
keyword: '',
@@ -554,7 +523,7 @@ const viewMainKvFields = computed<ViewField[]>(() => {
bxViewKvMoney('冲抵总金额(元)', formatMoneyYuan(d.offsetAmount || 0)),
bxViewKv('当前节点', workflowNodeDisplay(d.currentNode, d.status)),
bxViewKv('申请时间', d.applyTime),
bxViewKvLong('申请原因', d.reason)
bxViewKv('申请原因', d.reason, { long: false })
]
})
const selectedRows = ref<any[]>([])
@@ -567,56 +536,47 @@ const viewApprovalHistory = ref<any[]>([])
const viewFlowLoading = ref(false)
const viewOffsetRecords = ref<any[]>([])
const ADVANCE_APPLY_SUMMARY_COLS = 11
const ADVANCE_APPLY_SUMMARY_LABEL_COLSPAN = 4
function mergeAdvanceApplySummaryFooter() {
nextTick(() => {
const root = document.querySelector('.advance-apply-summary-merge .el-table__footer-wrapper')
if (!root) return
const tr = root.querySelector('tfoot tr') || root.querySelector('table tr')
if (!tr) return
const cells = Array.from(tr.querySelectorAll('td')) as HTMLTableCellElement[]
if (cells.length !== ADVANCE_APPLY_SUMMARY_COLS) return
const first = cells[0]
if (first.colSpan === ADVANCE_APPLY_SUMMARY_LABEL_COLSPAN) return
first.colSpan = ADVANCE_APPLY_SUMMARY_LABEL_COLSPAN
for (let i = ADVANCE_APPLY_SUMMARY_LABEL_COLSPAN - 1; i >= 1; i--) {
cells[i].remove()
}
})
}
function onSelectionChange(rows: any[]) {
selectedRows.value = rows
}
async function refreshListAndStats() {
async function refreshList() {
await refresh()
await loadMyAdvanceStats()
}
onMounted(() => {
refreshListAndStats()
refreshList()
})
watch([rows, loading], () => mergeAdvanceApplySummaryFooter(), { flush: 'post' })
function toggleMoreFilters() {
showMoreFilters.value = !showMoreFilters.value
function buildSearchQuery() {
const q: Record<string, string> = {}
const kw = (quickSearchKeyword.value.trim() || filterForm.keyword.trim())
if (kw) q.keyword = kw
if (filterForm.status) q.status = filterForm.status
if (filterForm.applyDate) q.applyDate = filterForm.applyDate
return q
}
function handleSearch() {
const q: Record<string, string> = { keyword: filterForm.keyword, status: filterForm.status }
if (filterForm.applyDate) q.applyDate = filterForm.applyDate
search(q)
quickSearchKeyword.value = ''
search(buildSearchQuery())
}
function handleReset() {
quickSearchKeyword.value = ''
Object.assign(filterForm, { keyword: '', status: '', amountRange: '', applyDate: null, currentNode: '' })
search()
}
function handleQuickSearch() {
search(buildSearchQuery())
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
search(buildSearchQuery())
}
function isDraftEditable(row?: any) {
if (!row) return false
return row.status === 'draft' || row.status === 'rejected'
@@ -659,7 +619,7 @@ async function handleToolbarDelete() {
await advanceApi.delete(row.id)
}
ElMessage.success('删除成功')
await refreshListAndStats()
await refreshList()
} catch (e: any) {
if (e !== 'cancel') {
ElMessage.error(e?.message || '删除失败')
@@ -717,7 +677,7 @@ async function handleToolbarSubmit() {
await advanceApi.submit(row.id)
}
ElMessage.success('提交成功')
await refreshListAndStats()
await refreshList()
} catch (e: any) {
ElMessage.error(e?.message || '提交失败')
} finally {
@@ -754,7 +714,7 @@ async function submitAdd() {
dialogs.add = false
Object.assign(addForm, { reason: '', amount: '', remark: '' })
editingId.value = null
await refreshListAndStats()
await refreshList()
} catch (e: any) {
ElMessage.error(e.message || (editingId.value ? '保存失败' : '新增失败'))
} finally {
@@ -819,7 +779,7 @@ async function handleSubmit(row: any) {
try {
await advanceApi.submit(row.id)
ElMessage.success('提交成功')
await refreshListAndStats()
await refreshList()
} catch (e: any) {
ElMessage.error(e.message || '提交失败')
} finally {
@@ -841,7 +801,7 @@ async function handleDelete(row: any) {
loadingMap[`del_${row.id}`] = true
await advanceApi.delete(row.id)
ElMessage.success('删除成功')
await refreshListAndStats()
await refreshList()
} catch (e: any) {
if (e !== 'cancel') {
ElMessage.error(e?.message || '删除失败')
@@ -851,28 +811,11 @@ async function handleDelete(row: any) {
}
}
function summaryMethod(params: { data: any[] }) {
const data = params.data
const n = data.length
const totalApply = data.reduce((s, r) => s + Number(r.amount || 0), 0)
const totalActual = data.reduce((s, r) => {
if (r.actualAmount != null && r.actualAmount !== '') return s + Number(r.actualAmount)
return s
}, 0)
const totalOffset = data.reduce((s, r) => s + Number(r.offsetAmount || 0), 0)
const lbl = `本页合计(${n}单)`
return [
lbl,
'',
'',
'',
'',
'',
formatSumMoney(totalApply),
formatSumMoney(totalActual),
formatSumMoney(totalOffset),
'',
''
]
}
const summaryMethod = buildPageSummaryMethod({
sumByProp: {
amount: (data) => sumField(data, 'amount'),
actualAmount: (data) => sumField(data, 'actualAmount', true),
offsetAmount: (data) => sumField(data, 'offsetAmount')
}
})
</script>
+146 -63
View File
@@ -2,13 +2,6 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<div class="stats">
<div v-for="item in stats" :key="item.name" class="stat" :class="{ 'stat-money': item.name.includes('金额') }">
<p class="k">{{ item.name }}</p>
<p class="v">{{ item.value }}</p>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">备用金审批</h2>
@@ -16,7 +9,7 @@
<el-tabs
v-model="activeTab"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleTabChange"
>
<!-- 待我审批 -->
@@ -67,6 +60,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="pendingQuickSearchKeyword"
placeholder="申请单号/申请原因"
@search="handlePendingQuickSearch"
@clear="handlePendingQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleBatchApprove">
<el-icon><Check /></el-icon>通过
</el-button>
@@ -74,7 +73,15 @@
<el-icon><Close /></el-icon>驳回
</el-button>
<ListToolbarEndActions :loading="pendingLoading" @refresh="refreshPending" />
<ListToolbarEndActions
:loading="pendingLoading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="refreshPending"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -94,6 +101,7 @@
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -104,6 +112,7 @@
<el-table-column
prop="applyCode"
label="申请单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -113,8 +122,16 @@
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('reason')"
prop="reason"
label="申请原因"
min-width="160"
@@ -123,6 +140,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="申请金额(元)"
:formatter="moneyTableFormatter"
@@ -131,7 +149,14 @@
align="right"
header-align="center"
/>
<el-table-column prop="currentNode" label="当前节点" min-width="120" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('currentNode')"
prop="currentNode"
label="当前节点"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="shouldShowWorkflowNodeTag(row.currentNode, row.status)"
@@ -203,6 +228,26 @@
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<ListQuickSearchInput
v-model="approvedQuickSearchKeyword"
placeholder="申请单号/申请原因"
@search="handleApprovedQuickSearch"
@clear="handleApprovedQuickSearchClear"
/>
<ListToolbarEndActions
:loading="approvedLoading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="handleApprovedSearch"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<div class="table-scroll table-page-summary">
<el-table
@@ -217,6 +262,7 @@
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -227,6 +273,7 @@
<el-table-column
prop="applyCode"
label="申请单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -236,8 +283,16 @@
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('reason')"
prop="reason"
label="申请原因"
min-width="160"
@@ -246,6 +301,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="申请金额(元)"
:formatter="moneyTableFormatter"
@@ -254,14 +310,28 @@
align="right"
header-align="center"
/>
<el-table-column prop="status" label="状态" min-width="100" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('status')"
prop="status"
label="状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">
{{ statusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="currentNode" label="当前节点" min-width="120" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('currentNode')"
prop="currentNode"
label="当前节点"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="shouldShowWorkflowNodeTag(row.currentNode, row.status)"
@@ -329,25 +399,12 @@
<BxViewKvTable :fields="viewMainKvFields" mode="dual" />
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="viewFlowNodes" :loading="viewFlowLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="viewApprovalHistory" :loading="viewFlowLoading" />
</div>
</div>
<BxApprovalFlowTabs
:nodes="viewFlowNodes"
:history="viewApprovalHistory"
:loading="viewFlowLoading"
/>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.view = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
<!-- 审批确认 -->
@@ -360,7 +417,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top">
<el-form label-position="right" label-width="100px" class="bx-dialog-form">
<div class="form-grid-2">
<el-form-item label="审批时间">
<el-input :model-value="approveTime" disabled />
@@ -394,16 +451,16 @@
<script setup lang="ts">
import { reactive, ref, computed, onMounted } from 'vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { ElMessage } from 'element-plus'
import { Check, Close, Search, RefreshLeft } from '@element-plus/icons-vue'
import { advanceApi } from '@/api/bx'
import { advanceApproverStats, refreshApproveMenuBadges } from '@/hooks/bx/useApproveMenuBadge'
import { refreshApproveMenuBadges } from '@/hooks/bx/useApproveMenuBadge'
import { useMenuStore } from '@/store/modules/menu'
import {
statusLabel,
statusTagType,
formatSumMoney,
formatMoney,
formatMoneyYuan,
moneyTableFormatter,
workflowNodeDisplay,
@@ -413,14 +470,31 @@ import {
} from '@/utils/bx/format'
import { useServerPaged } from '@/hooks/bx/useServerPaged'
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
import ApprovalHistoryView from '@/components/bx/base/ApprovalHistoryView.vue'
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import { bxViewKv, bxViewKvLong, bxViewKvMoney } from '@/utils/bx/view-kv'
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
type DialogKey = 'view' | 'approve'
const listColumnDefs = [
{ prop: 'applyTime', label: '申请时间' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'reason', label: '申请原因' },
{ prop: 'amount', label: '申请金额(元)' },
{ prop: 'status', label: '状态', defaultVisible: false },
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('bx-advance-approve-list-columns', listColumnDefs)
const loadingMap = reactive<Record<string, boolean>>({})
const dialogs = reactive<Record<DialogKey, boolean>>({
view: false,
@@ -459,15 +533,10 @@ const {
const tabLoaded = reactive({ pending: false, approved: false })
const activeTab = ref('pending')
const menuStore = useMenuStore()
const pendingQuickSearchKeyword = ref('')
const approvedQuickSearchKeyword = ref('')
const stats = computed(() => {
const s = advanceApproverStats.value
return [
{ name: '待审批', value: String(s.pendingTotal) },
{ name: '本月审批金额(元)', value: formatMoney(s.monthPendingAmount) }
]
})
const menuStore = useMenuStore()
const viewData = ref<Record<string, any>>({})
@@ -520,7 +589,7 @@ const approvedFilterForm = reactive({
function buildPendingQuery() {
const q: Record<string, string | number> = {}
const code = pendingFilterForm.code.trim()
const code = (pendingQuickSearchKeyword.value.trim() || pendingFilterForm.code.trim())
if (code) q.keyword = code
if (pendingFilterForm.applicantId != null) q.applicantId = pendingFilterForm.applicantId
if (pendingFilterForm.applyDate) q.applyDate = pendingFilterForm.applyDate
@@ -529,7 +598,7 @@ function buildPendingQuery() {
function buildApprovedQuery() {
const q: Record<string, string | number> = {}
const code = approvedFilterForm.code.trim()
const code = (approvedQuickSearchKeyword.value.trim() || approvedFilterForm.code.trim())
if (code) q.keyword = code
if (approvedFilterForm.applicantId != null) q.applicantId = approvedFilterForm.applicantId
if (approvedFilterForm.applyDate) q.applyDate = approvedFilterForm.applyDate
@@ -544,23 +613,45 @@ onMounted(async () => {
})
function handlePendingSearch() {
pendingQuickSearchKeyword.value = ''
searchPending(buildPendingQuery())
}
function handlePendingReset() {
pendingQuickSearchKeyword.value = ''
Object.assign(pendingFilterForm, { code: '', applicantId: undefined, applyDate: null })
searchPending()
}
function handlePendingQuickSearch() {
searchPending(buildPendingQuery())
}
function handlePendingQuickSearchClear() {
pendingQuickSearchKeyword.value = ''
searchPending(buildPendingQuery())
}
function handleApprovedSearch() {
approvedQuickSearchKeyword.value = ''
searchApproved(buildApprovedQuery())
}
function handleApprovedReset() {
approvedQuickSearchKeyword.value = ''
Object.assign(approvedFilterForm, { code: '', applicantId: undefined, applyDate: null })
searchApproved()
}
function handleApprovedQuickSearch() {
searchApproved(buildApprovedQuery())
}
function handleApprovedQuickSearchClear() {
approvedQuickSearchKeyword.value = ''
searchApproved(buildApprovedQuery())
}
function handleTabChange(tab: string | number) {
if (tab === 'pending' && !tabLoaded.pending) {
searchPending()
@@ -619,21 +710,13 @@ async function openView(row: any) {
}
}
function pendingSummaryMethod(params: { data: any[] }) {
const data = params.data
const n = data.length
const t = data.reduce((s, r) => s + Number(r.amount || 0), 0)
const lbl = `本页合计(${n}单)`
return ['', '', '', lbl, '', '', formatSumMoney(t), '', '', '']
}
function approvedSummaryMethod(params: { data: any[] }) {
const data = params.data
const n = data.length
const t = data.reduce((s, r) => s + Number(r.amount || 0), 0)
const lbl = `本页合计(${n}单)`
return ['', '', '', lbl, '', formatSumMoney(t), '', '', '', '']
}
const amountSummary = buildPageSummaryMethod({
sumByProp: {
amount: (data) => sumField(data, 'amount')
}
})
const pendingSummaryMethod = amountSummary
const approvedSummaryMethod = amountSummary
async function handleConfirmApprove() {
const key = approveAction.value === 'approve' ? 'approvePass' : 'approveReject'
+136 -74
View File
@@ -2,13 +2,6 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<div class="stats">
<div v-for="item in stats" :key="item.name" class="stat" :class="{ 'stat-money': item.name.includes('金额') }">
<p class="k">{{ item.name }}</p>
<p class="v">{{ item.value }}</p>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">备用金发放</h2>
@@ -16,7 +9,7 @@
<el-tabs
v-model="activeTab"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleTabChange"
>
<el-tab-pane label="待发放" name="pending">
@@ -66,13 +59,27 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="申请单号/申请原因"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleToolbarIssue">
<el-icon><Check /></el-icon>发放
</el-button>
<el-button class="btn-ghost" :loading="loadingMap.export" @click="handleExport">
<el-icon><Download /></el-icon>导出
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="handleSearch"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -93,6 +100,7 @@
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -103,6 +111,7 @@
<el-table-column
prop="applyCode"
label="申请单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -112,8 +121,16 @@
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('reason')"
prop="reason"
label="申请原因"
min-width="160"
@@ -122,6 +139,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="申请金额(元)"
:formatter="moneyTableFormatter"
@@ -131,6 +149,8 @@
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('actualAmount')"
prop="actualAmount"
label="实际发放金额(元)"
class-name="col-money"
min-width="130"
@@ -146,6 +166,8 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('offsetAmount')"
prop="offsetAmount"
label="冲抵总金额(元)"
class-name="col-money"
min-width="130"
@@ -157,6 +179,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('approveTime')"
prop="approveTime"
label="审批通过时间"
min-width="140"
@@ -225,11 +248,25 @@
<div class="list-toolbar">
<div class="list-toolbar-row">
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="申请单号/申请原因"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-ghost" :loading="loadingMap.export" @click="handleExport">
<el-icon><Download /></el-icon>导出
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="handleSearch"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -247,6 +284,7 @@
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -257,6 +295,7 @@
<el-table-column
prop="applyCode"
label="申请单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -266,8 +305,16 @@
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('reason')"
prop="reason"
label="申请原因"
min-width="160"
@@ -276,6 +323,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="申请金额(元)"
:formatter="moneyTableFormatter"
@@ -285,6 +333,8 @@
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('actualAmount')"
prop="actualAmount"
label="实际发放金额(元)"
class-name="col-money"
min-width="130"
@@ -300,6 +350,8 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('offsetAmount')"
prop="offsetAmount"
label="冲抵总金额(元)"
class-name="col-money"
min-width="130"
@@ -310,8 +362,22 @@
{{ formatSumMoney(Number(row.offsetAmount ?? 0)) }}
</template>
</el-table-column>
<el-table-column prop="issueTime" label="发放时间" min-width="140" align="center" header-align="center" />
<el-table-column prop="issueUserName" label="发放操作人" min-width="110" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('issueTime')"
prop="issueTime"
label="发放时间"
min-width="140"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('issueUserName')"
prop="issueUserName"
label="发放操作人"
min-width="110"
align="center"
header-align="center"
/>
</el-table>
</div>
@@ -341,7 +407,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top">
<el-form label-position="right" label-width="100px" class="bx-dialog-form">
<div class="form-grid-2">
<el-form-item label="发放金额(元)" class="field-amount">
<el-input :model-value="String(currentRow.amount || '')" disabled placeholder="发放金额" />
@@ -439,31 +505,20 @@
<BxAttachmentReadonly :files="viewAttachments" />
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="approvalNodes" :loading="approvalLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="approvalHistory" :loading="approvalLoading" />
</div>
</div>
<BxApprovalFlowTabs
:nodes="approvalNodes"
:history="approvalHistory"
:loading="approvalLoading"
/>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.view = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { reactive, ref, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Check, Download, Close, Search, RefreshLeft } from '@element-plus/icons-vue'
@@ -472,17 +527,37 @@ import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
import { formatSumMoney, formatMoney, formatMoneyYuan, moneyTableFormatter } from '@/utils/bx/format'
import { useServerPaged } from '@/hooks/bx/useServerPaged'
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
import ApprovalHistoryView from '@/components/bx/base/ApprovalHistoryView.vue'
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
import BaseDetailTableSection from '@/components/bx/base/BaseDetailTableSection.vue'
import BxAttachmentUpload from '@/components/bx/base/BxAttachmentUpload.vue'
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import { bxViewKv } from '@/utils/bx/view-kv'
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
type DialogKey = 'issue' | 'view'
const listColumnDefs = [
{ prop: 'applyTime', label: '申请时间' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'reason', label: '申请原因' },
{ prop: 'amount', label: '申请金额(元)' },
{ prop: 'actualAmount', label: '实际发放金额(元)' },
{ prop: 'offsetAmount', label: '冲抵总金额(元)', defaultVisible: false },
{ prop: 'approveTime', label: '审批通过时间', defaultVisible: false },
{ prop: 'issueTime', label: '发放时间', defaultVisible: false },
{ prop: 'issueUserName', label: '发放操作人', defaultVisible: false }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('bx-advance-issue-list-columns', listColumnDefs)
const loadingMap = reactive<Record<string, boolean>>({})
const dialogs = reactive<Record<DialogKey, boolean>>({
issue: false,
@@ -511,28 +586,7 @@ const { rows, total, currentPage, pageSize, rangeText, loading, refresh, search,
}
)
const stats = ref([
{ name: '待发放', value: '-' },
{ name: '已发放', value: '-' },
{ name: '本月发放金额(元)', value: '-' },
{ name: '待发放金额(元)', value: '-' }
])
async function loadAdvanceIssueStats() {
try {
const res: any = await paymentApi.advanceIssueStats()
const d = res.data
if (!d) return
stats.value = [
{ name: '待发放', value: String(d.pendingCount ?? 0) },
{ name: '已发放', value: String(d.issuedCount ?? 0) },
{ name: '本月发放金额(元)', value: formatMoney(d.issuedMonthAmount ?? 0) },
{ name: '待发放金额(元)', value: formatMoney(d.pendingAmount ?? 0) }
]
} catch {
/* interceptor */
}
}
const quickSearchKeyword = ref('')
const currentRow = ref<Record<string, any>>({})
const selectedRows = ref<any[]>([])
@@ -574,24 +628,17 @@ function issueStatusTagType(status: string): 'success' | 'warning' {
return status === 'issued' ? 'success' : 'warning'
}
function advanceIssueSummary(params: { data: any[] }) {
const data = params.data
const n = data.length
const tAmt = data.reduce((s, r) => s + Number(r.amount || 0), 0)
const tAct = data.reduce((s, r) => s + Number(r.actualAmount ?? 0), 0)
const tOff = data.reduce((s, r) => s + Number(r.offsetAmount ?? 0), 0)
const lbl = `本页合计(${n}单)`
const body = ['', '', '', lbl, '', '', formatSumMoney(tAmt), formatSumMoney(tAct), formatSumMoney(tOff)]
if (activeTab.value === 'pending') {
return [...body, '', '']
const advanceIssueSummary = buildPageSummaryMethod({
sumByProp: {
amount: (data) => sumField(data, 'amount'),
actualAmount: (data) => sumField(data, 'actualAmount'),
offsetAmount: (data) => sumField(data, 'offsetAmount')
}
return [...body, '', '', '']
}
})
onMounted(() => {
loadApplicantOptions()
refresh()
loadAdvanceIssueStats()
})
function onSelectionChange(rows: any[]) {
@@ -608,24 +655,40 @@ function handleToolbarIssue() {
function handleTabChange(tab: string | number) {
activeTab.value = String(tab)
quickSearchKeyword.value = ''
Object.assign(filterForm, { keyword: '', applicantId: undefined, applyDate: null })
search()
}
function handleSearch() {
function buildSearchQuery() {
const q: Record<string, string | number> = {}
const kw = filterForm.keyword.trim()
const kw = (quickSearchKeyword.value.trim() || filterForm.keyword.trim())
if (kw) q.keyword = kw
if (filterForm.applicantId != null) q.applicantId = filterForm.applicantId
if (filterForm.applyDate) q.applyDate = filterForm.applyDate
search(q)
return q
}
function handleSearch() {
quickSearchKeyword.value = ''
search(buildSearchQuery())
}
function handleReset() {
quickSearchKeyword.value = ''
Object.assign(filterForm, { keyword: '', applicantId: undefined, applyDate: null })
search()
}
function handleQuickSearch() {
search(buildSearchQuery())
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
search(buildSearchQuery())
}
async function handleExport() {
loadingMap.export = true
try {
@@ -678,7 +741,6 @@ async function submitIssue() {
ElMessage.success('发放成功')
dialogs.issue = false
await refresh()
await loadAdvanceIssueStats()
} catch (e: any) {
ElMessage.error(e.message || '发放失败')
} finally {
+93 -9
View File
@@ -48,6 +48,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="项目名称/项目编码"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="openAddDialog">
<el-icon><Plus /></el-icon>新增一级项目
</el-button>
@@ -84,7 +90,15 @@
<el-icon><SortUp /></el-icon>收起全部
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="handleSearch"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -98,6 +112,7 @@
row-key="id"
:tree-props="{ children: 'children', hasChildren: 'childCount > 0' }"
@selection-change="onSelectionChange"
@row-dblclick="openEditDialog"
>
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
@@ -111,6 +126,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('itemCode')"
prop="itemCode"
label="项目编码"
min-width="160"
@@ -118,7 +134,14 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column prop="level" label="层级" min-width="100" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('level')"
prop="level"
label="层级"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="row.level === 1 ? 'primary' : 'info'" size="small">
{{ row.level === 1 ? '一级' : '二级' }}
@@ -126,14 +149,29 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('childCount')"
prop="childCount"
label="子项数量"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column prop="sortOrder" label="排序号" min-width="80" align="center" header-align="center" />
<el-table-column prop="status" label="状态" min-width="100" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('sortOrder')"
prop="sortOrder"
label="排序号"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('status')"
prop="status"
label="状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">
{{ statusLabel(row.status) }}
@@ -159,7 +197,9 @@
ref="addFormRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="bx-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -211,7 +251,9 @@
ref="editFormRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="bx-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -277,7 +319,9 @@
ref="addChildFormRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="bx-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -321,6 +365,8 @@
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { reactive, ref, onMounted, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance } from 'element-plus'
@@ -349,6 +395,24 @@ const dialogs = reactive<Record<DialogKey, boolean>>({
addChild: false
})
const listColumnDefs = [
{ prop: 'itemCode', label: '项目编码' },
{ prop: 'level', label: '层级' },
{ prop: 'childCount', label: '子项数量' },
{ prop: 'sortOrder', label: '排序号', defaultVisible: false },
{ prop: 'status', label: '状态' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('bx-expense-item-list-columns', listColumnDefs)
const quickSearchKeyword = ref('')
const searchForm = reactive({
name: '',
code: '',
@@ -396,6 +460,7 @@ function buildSubmitPayload() {
}
function filterTreeNodes(nodes: any[]): any[] {
const qk = quickSearchKeyword.value.trim().toLowerCase()
return nodes
.map((node) => {
const children = node.children?.length ? filterTreeNodes(node.children) : []
@@ -403,7 +468,15 @@ function filterTreeNodes(nodes: any[]): any[] {
const codeOk = !searchForm.code || String(node.itemCode || '').includes(searchForm.code)
const levelOk = !searchForm.level || String(node.level) === searchForm.level
const statusOk = !searchForm.status || String(node.status) === searchForm.status
const selfMatch = nameOk && codeOk && levelOk && statusOk
const quickOk =
!qk ||
String(node.itemName || '')
.toLowerCase()
.includes(qk) ||
String(node.itemCode || '')
.toLowerCase()
.includes(qk)
const selfMatch = nameOk && codeOk && levelOk && statusOk && quickOk
if (selfMatch || children.length) {
return { ...node, children: children.length ? children : node.children?.length ? [] : undefined }
}
@@ -413,7 +486,8 @@ function filterTreeNodes(nodes: any[]): any[] {
}
const filteredTree = computed(() => {
const hasFilter = searchForm.name || searchForm.code || searchForm.level || searchForm.status
const hasFilter =
searchForm.name || searchForm.code || searchForm.level || searchForm.status || quickSearchKeyword.value.trim()
if (!hasFilter) return expenseTree.value
return filterTreeNodes(expenseTree.value)
})
@@ -597,10 +671,12 @@ function collapseAll() {
}
function handleSearch() {
quickSearchKeyword.value = ''
loadTree()
}
function handleReset() {
quickSearchKeyword.value = ''
searchForm.name = ''
searchForm.code = ''
searchForm.level = ''
@@ -608,6 +684,14 @@ function handleReset() {
loadTree()
}
function handleQuickSearch() {
/* filteredTree reacts to quickSearchKeyword */
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
}
watch(
() => formData.level,
(level) => {
+192 -93
View File
@@ -2,13 +2,6 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<div class="stats">
<div v-for="item in stats" :key="item.name" class="stat" :class="{ 'stat-money': item.name.includes('金额') }">
<p class="k">{{ item.name }}</p>
<p class="v">{{ item.value }}</p>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">支付管理</h2>
@@ -16,7 +9,7 @@
<el-tabs
v-model="activeTab"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleTabChange"
>
<!-- 待处理 -->
@@ -67,13 +60,27 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="单号/报销标题"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleToolbarIssue">
<el-icon><Money /></el-icon>发放
</el-button>
<el-button class="btn-ghost" @click="handleToolbarOffset">
<el-icon><Switch /></el-icon>冲抵备用金
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="handleSearch"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -94,6 +101,7 @@
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -104,6 +112,7 @@
<el-table-column
prop="applyCode"
label="单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -113,8 +122,16 @@
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('title')"
prop="title"
label="报销标题"
min-width="180"
@@ -123,6 +140,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="报销金额(元)"
:formatter="moneyTableFormatter"
@@ -131,12 +149,28 @@
align="right"
header-align="center"
/>
<el-table-column label="冲抵金额(元)" class-name="col-money" min-width="120" align="right" header-align="center">
<el-table-column
v-if="isColumnVisible('offsetAmount')"
prop="offsetAmount"
label="冲抵金额(元)"
class-name="col-money"
min-width="120"
align="right"
header-align="center"
>
<template #default="{ row }">
{{ formatSumMoney(Number(row.offsetAmount || 0)) }}
</template>
</el-table-column>
<el-table-column label="发放金额(元)" class-name="col-money" min-width="120" align="right" header-align="center">
<el-table-column
v-if="isColumnVisible('payAmount')"
prop="actualAmount"
label="发放金额(元)"
class-name="col-money"
min-width="120"
align="right"
header-align="center"
>
<template #default="{ row }">
{{
row.actualAmount != null && row.actualAmount !== ''
@@ -215,6 +249,26 @@
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<ListQuickSearchInput
v-model="processedQuickSearchKeyword"
placeholder="单号/报销标题"
@search="handleProcessedQuickSearch"
@clear="handleProcessedQuickSearchClear"
/>
<ListToolbarEndActions
:loading="processedLoading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="handleProcessedSearch"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<div class="table-scroll table-page-summary">
<el-table
@@ -229,6 +283,7 @@
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -239,6 +294,7 @@
<el-table-column
prop="applyCode"
label="单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -248,8 +304,16 @@
<el-link type="primary" @click="openProcessedView(row)">{{ row.applyCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('title')"
prop="title"
label="标题"
min-width="180"
@@ -258,6 +322,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="报销金额(元)"
:formatter="moneyTableFormatter"
@@ -266,12 +331,28 @@
align="right"
header-align="center"
/>
<el-table-column label="冲抵金额(元)" class-name="col-money" min-width="120" align="right" header-align="center">
<el-table-column
v-if="isColumnVisible('offsetAmount')"
prop="offsetAmount"
label="冲抵金额(元)"
class-name="col-money"
min-width="120"
align="right"
header-align="center"
>
<template #default="{ row }">
{{ formatSumMoney(Number(row.offsetAmount || 0)) }}
</template>
</el-table-column>
<el-table-column label="发放金额(元)" class-name="col-money" min-width="120" align="right" header-align="center">
<el-table-column
v-if="isColumnVisible('payAmount')"
prop="actualAmount"
label="发放金额(元)"
class-name="col-money"
min-width="120"
align="right"
header-align="center"
>
<template #default="{ row }">
{{
row.actualAmount != null && row.actualAmount !== ''
@@ -280,14 +361,27 @@
}}
</template>
</el-table-column>
<el-table-column label="处理状态" min-width="100" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('processStatus')"
label="处理状态"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="processedStatusTagType(row.status, row.payStatus)" size="small">
{{ processedStatusLabel(row.status, row.payStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="payTime" label="处理时间" min-width="160" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('payTime')"
prop="payTime"
label="处理时间"
min-width="160"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ row.payTime || '-' }}
</template>
@@ -355,11 +449,6 @@
<BxAttachmentReadonly :files="viewAttachments" />
</div>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.viewDetail = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
<!-- 标记已发放 -->
@@ -372,7 +461,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top">
<el-form label-position="right" label-width="100px" class="bx-dialog-form">
<div class="form-grid-2">
<el-form-item label="发放金额(元)" class="field-amount">
<el-input :model-value="String(currentRow.amount || '')" disabled placeholder="发放金额" />
@@ -417,7 +506,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top">
<el-form label-position="right" label-width="100px" class="bx-dialog-form">
<h4 class="dialog-section-title">报销单信息</h4>
<div class="form-grid-2">
<el-form-item label="报销单号">
@@ -485,7 +574,7 @@
/>
</div>
<el-form-item label="备注" style="margin-top: 16px">
<el-form-item label="备注">
<el-input v-model="offsetRemark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
@@ -577,31 +666,20 @@
</BaseDetailTableSection>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="processedApprovals" :loading="processedApprovalLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="processedApprovalHistory" :loading="processedApprovalLoading" />
</div>
</div>
<BxApprovalFlowTabs
:nodes="processedApprovals"
:history="processedApprovalHistory"
:loading="processedApprovalLoading"
/>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.processedDetail = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { reactive, ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
@@ -618,8 +696,7 @@ import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
import { statusLabel, statusTagType, formatSumMoney, formatMoney, formatMoneyYuan, moneyTableFormatter } from '@/utils/bx/format'
import { useServerPaged } from '@/hooks/bx/useServerPaged'
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
import ApprovalHistoryView from '@/components/bx/base/ApprovalHistoryView.vue'
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
import BxViewKvGrid from '@/components/bx/base/BxViewKvGrid.vue'
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
import BaseDetailTableSection from '@/components/bx/base/BaseDetailTableSection.vue'
@@ -627,9 +704,29 @@ import BxAttachmentUpload from '@/components/bx/base/BxAttachmentUpload.vue'
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import { bxViewKv, bxViewKvMoney } from '@/utils/bx/view-kv'
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
type DialogKey = 'viewDetail' | 'issue' | 'offset' | 'processedDetail'
const listColumnDefs = [
{ prop: 'applyTime', label: '申请时间' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'title', label: '报销标题' },
{ prop: 'amount', label: '报销金额(元)' },
{ prop: 'offsetAmount', label: '冲抵金额(元)', defaultVisible: false },
{ prop: 'payAmount', label: '发放金额(元)' },
{ prop: 'processStatus', label: '处理状态', defaultVisible: false },
{ prop: 'payTime', label: '处理时间', defaultVisible: false }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('bx-payment-list-columns', listColumnDefs)
const { applicantOptions, loadApplicantOptions } = useApplicantOptions()
const loadingMap = reactive<Record<string, boolean>>({})
const dialogs = reactive<Record<DialogKey, boolean>>({
@@ -652,28 +749,8 @@ const { rows, total, currentPage, pageSize, rangeText, loading, refresh, search,
}
)
const stats = ref([
{ name: '待处理单据', value: '-' },
{ name: '已打款', value: '-' },
{ name: '冲抵备用金', value: '-' },
{ name: '本月处理金额(元)', value: '-' }
])
async function loadPaymentFinanceStats() {
try {
const res: any = await paymentApi.financeStats()
const d = res.data
if (!d) return
stats.value = [
{ name: '待处理单据', value: String(d.pendingCount ?? 0) },
{ name: '已打款', value: String(d.paidCount ?? 0) },
{ name: '冲抵备用金', value: String(d.offsetCount ?? 0) },
{ name: '本月处理金额(元)', value: formatMoney(d.monthProcessedAmount ?? 0) }
]
} catch {
/* interceptor */
}
}
const quickSearchKeyword = ref('')
const processedQuickSearchKeyword = ref('')
const filterForm = reactive({
code: '',
@@ -836,7 +913,6 @@ const processedOffsetRecords = ref<any[]>([])
onMounted(() => {
loadApplicantOptions()
refresh()
loadPaymentFinanceStats()
})
function onSelectionChange(rows: any[]) {
@@ -859,20 +935,35 @@ function handleToolbarOffset() {
openOffsetDialog(selectedRows.value[0])
}
function handleSearch() {
function buildPendingQuery() {
const q: Record<string, string | number> = {}
const kw = filterForm.code.trim()
const kw = (quickSearchKeyword.value.trim() || filterForm.code.trim())
if (kw) q.keyword = kw
if (filterForm.applicantId != null) q.applicantId = filterForm.applicantId
if (filterForm.applyDate) q.applyDate = filterForm.applyDate
search(q)
return q
}
function handleSearch() {
quickSearchKeyword.value = ''
search(buildPendingQuery())
}
function handleReset() {
quickSearchKeyword.value = ''
Object.assign(filterForm, { code: '', applicantId: undefined, applyDate: null, status: '' })
search()
}
function handleQuickSearch() {
search(buildPendingQuery())
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
search(buildPendingQuery())
}
function handleTabChange(tab: string | number) {
if (tab === 'processed' && !tabLoaded.processed) {
processedSearch()
@@ -880,15 +971,35 @@ function handleTabChange(tab: string | number) {
}
}
function buildProcessedQuery() {
const q: Record<string, string | number> = {}
const kw = (processedQuickSearchKeyword.value.trim() || processedFilterForm.code.trim())
if (kw) q.keyword = kw
if (processedFilterForm.applicantId != null) q.applicantId = processedFilterForm.applicantId
if (processedFilterForm.applyDate) q.applyDate = processedFilterForm.applyDate
return q
}
function handleProcessedSearch() {
processedSearch()
processedQuickSearchKeyword.value = ''
processedSearch(buildProcessedQuery())
}
function handleProcessedReset() {
processedQuickSearchKeyword.value = ''
Object.assign(processedFilterForm, { code: '', applicantId: undefined, applyDate: null, status: '' })
processedSearch()
}
function handleProcessedQuickSearch() {
processedSearch(buildProcessedQuery())
}
function handleProcessedQuickSearchClear() {
processedQuickSearchKeyword.value = ''
processedSearch(buildProcessedQuery())
}
async function openView(row: any) {
viewData.value = row
viewAttachments.value = []
@@ -928,7 +1039,6 @@ async function submitIssue() {
ElMessage.success('发放成功')
dialogs.issue = false
await refresh()
await loadPaymentFinanceStats()
} catch (e: any) {
ElMessage.error(e.message || '发放失败')
} finally {
@@ -1071,7 +1181,6 @@ async function submitOffset() {
ElMessage.success('冲抵成功')
dialogs.offset = false
await refresh()
await loadPaymentFinanceStats()
} catch (e: any) {
ElMessage.error(e.message || '冲抵失败')
} finally {
@@ -1079,25 +1188,15 @@ async function submitOffset() {
}
}
function summaryMethod(params: { data: any[] }) {
const data = params.data
const n = data.length
const tr = data.reduce((s, r) => s + Number(r.amount || 0), 0)
const toff = data.reduce((s, r) => s + Number(r.offsetAmount || 0), 0)
const tpay = data.reduce((s, r) => s + Number(r.actualAmount ?? 0), 0)
const lbl = `本页合计(${n}单)`
return ['', '', '', lbl, '', '', formatSumMoney(tr), formatSumMoney(toff), formatSumMoney(tpay), '']
}
function processedSummaryMethod(params: { data: any[] }) {
const data = params.data
const n = data.length
const tr = data.reduce((s, r) => s + Number(r.amount || 0), 0)
const toff = data.reduce((s, r) => s + Number(r.offsetAmount || 0), 0)
const tpay = data.reduce((s, r) => s + Number(r.actualAmount ?? 0), 0)
const lbl = `本页合计(${n}单)`
return ['', '', '', lbl, '', formatSumMoney(tr), formatSumMoney(toff), formatSumMoney(tpay), '', '', '']
}
const paymentSummary = buildPageSummaryMethod({
sumByProp: {
amount: (data) => sumField(data, 'amount'),
offsetAmount: (data) => sumField(data, 'offsetAmount'),
actualAmount: (data) => sumField(data, 'actualAmount')
}
})
const summaryMethod = paymentSummary
const processedSummaryMethod = paymentSummary
async function openProcessedView(row: any) {
processedViewData.value = row
@@ -2,13 +2,6 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<div class="stats">
<div v-for="item in stats" :key="item.name" class="stat" :class="{ 'stat-money': item.name.includes('金额') }">
<p class="k">{{ item.name }}</p>
<p class="v">{{ item.value }}</p>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">报销申请列表</h2>
@@ -60,6 +53,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="报销单号/标题/费用项目"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="openAddDialog">
<el-icon><Plus /></el-icon>新增报销申请
</el-button>
@@ -89,7 +88,15 @@
<el-button class="btn-ghost" @click="openFlowDialog()">
<el-icon><View /></el-icon>查看审批流
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="handleSearch"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -110,6 +117,7 @@
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -120,6 +128,7 @@
<el-table-column
prop="applyCode"
label="报销单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -130,6 +139,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('title')"
prop="title"
label="报销标题"
min-width="180"
@@ -138,6 +148,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('expenseName')"
prop="expenseName"
label="费用项目"
min-width="140"
@@ -146,6 +157,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="报销金额(元)"
:formatter="moneyTableFormatter"
@@ -154,14 +166,28 @@
align="right"
header-align="center"
/>
<el-table-column prop="status" label="状态" min-width="110" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('status')"
prop="status"
label="状态"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">
{{ statusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="currentNode" label="当前节点" min-width="120" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('currentNode')"
prop="currentNode"
label="当前节点"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="shouldShowWorkflowNodeTag(row.currentNode, row.status)"
@@ -202,7 +228,7 @@
>
<p class="dlg-note">提交后进入顺序审批流程,可在「查看审批流」中确认节点与审批人。</p>
<el-form label-position="top">
<el-form label-position="right" label-width="100px" class="bx-dialog-form">
<div class="form-grid-2">
<el-form-item label="报销日期">
<el-date-picker
@@ -299,7 +325,7 @@
/>
</div>
<el-form label-position="top">
<el-form label-position="right" label-width="100px" class="bx-dialog-form">
<el-form-item label="备注说明">
<el-input v-model="addForm.remark" type="textarea" :rows="4" placeholder="请输入报销说明" />
</el-form-item>
@@ -424,25 +450,12 @@
<BxAttachmentReadonly :files="viewOffsetAttachments" />
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="viewFlowNodes" :loading="viewFlowLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="viewApprovalHistory" :loading="viewFlowLoading" />
</div>
</div>
<BxApprovalFlowTabs
:nodes="viewFlowNodes"
:history="viewApprovalHistory"
:loading="viewFlowLoading"
/>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.view = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
<el-dialog
@@ -453,29 +466,15 @@
align-center
destroy-on-close
>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="flowNodes" :loading="flowLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="flowHistory" :loading="flowLoading" />
</div>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.viewFlow = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
<BxApprovalFlowTabs :nodes="flowNodes" :history="flowHistory" :loading="flowLoading" />
</el-dialog>
</div>
</template>
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { reactive, ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
@@ -496,8 +495,6 @@ import { useExpenseItemTree, type ExpenseItem } from '@/hooks/bx/useExpenseItemT
import {
statusLabel,
statusTagType,
formatSumMoney,
formatMoney,
formatMoneyYuan,
moneyTableFormatter,
workflowNodeDisplay,
@@ -506,14 +503,14 @@ import {
shouldShowWorkflowNodeTag
} from '@/utils/bx/format'
import { useServerPaged } from '@/hooks/bx/useServerPaged'
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
import ApprovalHistoryView from '@/components/bx/base/ApprovalHistoryView.vue'
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
import BxViewKvGrid from '@/components/bx/base/BxViewKvGrid.vue'
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
import BxAttachmentUpload from '@/components/bx/base/BxAttachmentUpload.vue'
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import { bxViewKv, bxViewKvLong, bxViewKvMoney } from '@/utils/bx/view-kv'
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
type DialogKey = 'add' | 'view' | 'viewFlow'
@@ -549,37 +546,24 @@ function markDisabledExpenseNodes(tree: ExpenseItem[]): ExpenseItem[] {
const expenseTreeForSelect = computed(() => markDisabledExpenseNodes(expenseTree.value))
const myReimburseStats = ref({
pendingCount: 0,
monthApplyAmount: 0,
approvedAmount: 0,
approvedCount: 0
})
const quickSearchKeyword = ref('')
async function loadMyReimburseStats() {
try {
const res: any = await reimburseApi.myStats()
const d = res.data || {}
myReimburseStats.value = {
pendingCount: Number(d.pendingCount ?? 0),
monthApplyAmount: Number(d.monthApplyAmount ?? 0),
approvedAmount: Number(d.approvedAmount ?? 0),
approvedCount: Number(d.approvedCount ?? 0)
}
} catch {
/* 拦截器已提示 */
}
}
const listColumnDefs = [
{ prop: 'applyTime', label: '申请时间' },
{ prop: 'title', label: '报销标题' },
{ prop: 'expenseName', label: '费用项目' },
{ prop: 'amount', label: '报销金额(元)' },
{ prop: 'status', label: '状态' },
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
]
const stats = computed(() => {
const s = myReimburseStats.value
return [
{ name: '待审批申请', value: String(s.pendingCount) },
{ name: '本月申请金额(元)', value: formatMoney(s.monthApplyAmount) },
{ name: '已审批金额(元)', value: formatMoney(s.approvedAmount) },
{ name: '已通过申请', value: String(s.approvedCount) }
]
})
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('bx-reimburse-apply-list-columns', listColumnDefs)
const filterForm = reactive({
keyword: '',
@@ -730,7 +714,7 @@ async function handleToolbarDelete() {
await reimburseApi.delete(row.id)
}
ElMessage.success('删除成功')
await refreshListAndStats()
await refreshList()
} catch (e: any) {
if (e !== 'cancel') {
ElMessage.error(e?.message || '删除失败')
@@ -796,26 +780,44 @@ function onSelectionChange(rows: any[]) {
selectedRows.value = rows
}
async function refreshListAndStats() {
async function refreshList() {
await refresh()
await loadMyReimburseStats()
}
onMounted(() => {
refreshListAndStats()
refreshList()
})
function handleSearch() {
const q: Record<string, string> = { keyword: filterForm.keyword, status: filterForm.status }
function buildSearchQuery() {
const q: Record<string, string> = {}
const kw = (quickSearchKeyword.value.trim() || filterForm.keyword.trim())
if (kw) q.keyword = kw
if (filterForm.status) q.status = filterForm.status
if (filterForm.expenseName) q.expenseName = filterForm.expenseName
if (filterForm.applyDate) q.applyDate = filterForm.applyDate
search(q)
return q
}
function handleSearch() {
quickSearchKeyword.value = ''
search(buildSearchQuery())
}
function handleReset() {
quickSearchKeyword.value = ''
Object.assign(filterForm, { keyword: '', status: '', expenseName: '', applyDate: null })
search()
}
function handleQuickSearch() {
search(buildSearchQuery())
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
search(buildSearchQuery())
}
async function handleToolbarSubmit() {
if (selectedRows.value.length === 0) {
ElMessage.warning('请先勾选要提交的报销单')
@@ -827,7 +829,7 @@ async function handleToolbarSubmit() {
await reimburseApi.submit(row.id)
}
ElMessage.success('提交成功')
await refreshListAndStats()
await refreshList()
} catch (e: any) {
ElMessage.error(e?.message || '提交失败')
} finally {
@@ -894,7 +896,7 @@ async function submitAdd() {
addForm.details = [{ expenseItemId: '', amount: '', remark: '' }]
uploadedFiles.value = []
editingId.value = null
await refreshListAndStats()
await refreshList()
} catch (e: any) {
ElMessage.error(e.message || (editingId.value ? '保存失败' : '新增失败'))
} finally {
@@ -982,7 +984,7 @@ async function handleSubmit(row: any) {
try {
await reimburseApi.submit(row.id)
ElMessage.success('提交成功')
await refreshListAndStats()
await refreshList()
} catch (e: any) {
ElMessage.error(e.message || '提交失败')
} finally {
@@ -1004,7 +1006,7 @@ async function handleDelete(row: any) {
loadingMap[`del_${row.id}`] = true
await reimburseApi.delete(row.id)
ElMessage.success('删除成功')
await refreshListAndStats()
await refreshList()
} catch (e: any) {
if (e !== 'cancel') {
ElMessage.error(e?.message || '删除失败')
@@ -1014,13 +1016,11 @@ async function handleDelete(row: any) {
}
}
function summaryMethod(params: { data: any[] }) {
const data = params.data
const n = data.length
const totalAmt = data.reduce((s, r) => s + Number(r.amount || 0), 0)
const lbl = `本页合计(${n}单)`
return ['', '', '', lbl, '', '', formatSumMoney(totalAmt), '', '']
}
const summaryMethod = buildPageSummaryMethod({
sumByProp: {
amount: (data) => sumField(data, 'amount')
}
})
function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
const sums: string[] = []
+172 -134
View File
@@ -2,13 +2,6 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<div class="stats">
<div v-for="item in stats" :key="item.name" class="stat" :class="{ 'stat-money': item.name.includes('金额') }">
<p class="k">{{ item.name }}</p>
<p class="v">{{ item.value }}</p>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">报销审批</h2>
@@ -16,7 +9,7 @@
<el-tabs
v-model="activeTab"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleTabChange"
>
<!-- 待我审批 -->
@@ -64,41 +57,19 @@
style="width: 100%"
/>
</el-form-item>
</div>
<div
v-show="showMorePendingFilters"
id="reimburse-approve-pending-filter-extra"
class="filter-extra"
>
<div class="filter-grid">
<el-form-item label="当前节点">
<el-select v-model="pendingFilterForm.currentNode" placeholder="请选择节点" clearable>
<el-option label="发起人主管审批" value="发起人主管审批" />
<el-option label="财务复核" value="财务复核" />
<el-option label="总经理审批" value="总经理审批" />
</el-select>
</el-form-item>
<el-form-item label="金额区间(元)">
<el-input v-model="pendingFilterForm.amountRange" placeholder="如 1000-5000" clearable />
</el-form-item>
</div>
<el-form-item label="当前节点">
<el-select v-model="pendingFilterForm.currentNode" placeholder="请选择节点" clearable>
<el-option label="发起人主管审批" value="发起人主管审批" />
<el-option label="财务复核" value="财务复核" />
<el-option label="总经理审批" value="总经理审批" />
</el-select>
</el-form-item>
<el-form-item label="金额区间(元)">
<el-input v-model="pendingFilterForm.amountRange" placeholder="如 1000-5000" clearable />
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="showMorePendingFilters"
aria-controls="reimburse-approve-pending-filter-extra"
@click="showMorePendingFilters = !showMorePendingFilters"
>
{{ showMorePendingFilters ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="showMorePendingFilters" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="pendingLoading" @click="handlePendingReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -116,6 +87,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="pendingQuickSearchKeyword"
placeholder="报销单号/报销标题"
@search="handlePendingQuickSearch"
@clear="handlePendingQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleBatchPass">
<el-icon><Check /></el-icon>通过
</el-button>
@@ -123,7 +100,15 @@
<el-icon><Close /></el-icon>驳回
</el-button>
<ListToolbarEndActions :loading="pendingLoading" @refresh="refreshPending" />
<ListToolbarEndActions
:loading="pendingLoading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="refreshPending"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -143,6 +128,7 @@
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -153,6 +139,7 @@
<el-table-column
prop="applyCode"
label="报销单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -162,8 +149,16 @@
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('title')"
prop="title"
label="报销标题"
min-width="180"
@@ -172,6 +167,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('expenseName')"
prop="expenseName"
label="费用项目"
min-width="140"
@@ -180,6 +176,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="报销金额(元)"
:formatter="moneyTableFormatter"
@@ -188,7 +185,14 @@
align="right"
header-align="center"
/>
<el-table-column prop="currentNode" label="当前节点" min-width="120" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('currentNode')"
prop="currentNode"
label="当前节点"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="shouldShowWorkflowNodeTag(row.currentNode, row.status)"
@@ -262,41 +266,19 @@
style="width: 100%"
/>
</el-form-item>
</div>
<div
v-show="showMoreApprovedFilters"
id="reimburse-approve-approved-filter-extra"
class="filter-extra"
>
<div class="filter-grid">
<el-form-item label="当前节点">
<el-select v-model="approvedFilterForm.currentNode" placeholder="请选择节点" clearable>
<el-option label="发起人主管审批" value="发起人主管审批" />
<el-option label="财务复核" value="财务复核" />
<el-option label="总经理审批" value="总经理审批" />
</el-select>
</el-form-item>
<el-form-item label="金额区间(元)">
<el-input v-model="approvedFilterForm.amountRange" placeholder="如 1000-5000" clearable />
</el-form-item>
</div>
<el-form-item label="当前节点">
<el-select v-model="approvedFilterForm.currentNode" placeholder="请选择节点" clearable>
<el-option label="发起人主管审批" value="发起人主管审批" />
<el-option label="财务复核" value="财务复核" />
<el-option label="总经理审批" value="总经理审批" />
</el-select>
</el-form-item>
<el-form-item label="金额区间(元)">
<el-input v-model="approvedFilterForm.amountRange" placeholder="如 1000-5000" clearable />
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="showMoreApprovedFilters"
aria-controls="reimburse-approve-approved-filter-extra"
@click="showMoreApprovedFilters = !showMoreApprovedFilters"
>
{{ showMoreApprovedFilters ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="showMoreApprovedFilters" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="approvedLoading" @click="handleApprovedReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -309,6 +291,26 @@
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<ListQuickSearchInput
v-model="approvedQuickSearchKeyword"
placeholder="报销单号/报销标题"
@search="handleApprovedQuickSearch"
@clear="handleApprovedQuickSearchClear"
/>
<ListToolbarEndActions
:loading="approvedLoading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="handleApprovedSearch"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<div class="table-scroll table-page-summary">
<el-table
@@ -323,6 +325,7 @@
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -333,6 +336,7 @@
<el-table-column
prop="applyCode"
label="报销单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -342,8 +346,16 @@
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('title')"
prop="title"
label="报销标题"
min-width="180"
@@ -352,6 +364,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('expenseName')"
prop="expenseName"
label="费用项目"
min-width="140"
@@ -360,6 +373,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="报销金额(元)"
:formatter="moneyTableFormatter"
@@ -368,14 +382,28 @@
align="right"
header-align="center"
/>
<el-table-column prop="status" label="状态" min-width="110" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('status')"
prop="status"
label="状态"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">
{{ statusLabel(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="currentNode" label="当前节点" min-width="120" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('currentNode')"
prop="currentNode"
label="当前节点"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="shouldShowWorkflowNodeTag(row.currentNode, row.status)"
@@ -473,25 +501,12 @@
<BxAttachmentReadonly :files="viewAttachments" />
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="viewFlowNodes" :loading="viewFlowLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="viewApprovalHistory" :loading="viewFlowLoading" />
</div>
</div>
<BxApprovalFlowTabs
:nodes="viewFlowNodes"
:history="viewApprovalHistory"
:loading="viewFlowLoading"
/>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.viewDetail = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
<!-- 审批确认 -->
@@ -504,7 +519,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top">
<el-form label-position="right" label-width="100px" class="bx-dialog-form">
<div class="form-grid-2">
<el-form-item label="审批时间">
<el-input :model-value="approveTime" disabled />
@@ -538,25 +553,23 @@
<script setup lang="ts">
import { reactive, ref, computed, onMounted } from 'vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { ElMessage } from 'element-plus'
import {
View,
Check,
Close,
Search,
RefreshLeft,
ArrowUpBold,
ArrowDownBold
RefreshLeft
} from '@element-plus/icons-vue'
import { reimburseApi } from '@/api/bx'
import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
import { reimburseApproverStats, refreshApproveMenuBadges } from '@/hooks/bx/useApproveMenuBadge'
import { refreshApproveMenuBadges } from '@/hooks/bx/useApproveMenuBadge'
import { useMenuStore } from '@/store/modules/menu'
import {
statusLabel,
statusTagType,
formatSumMoney,
formatMoney,
formatMoneyYuan,
moneyTableFormatter,
workflowNodeDisplay,
@@ -567,17 +580,36 @@ import {
import { useServerPaged } from '@/hooks/bx/useServerPaged'
import { useExpenseItemTree } from '@/hooks/bx/useExpenseItemTree'
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
import ApprovalHistoryView from '@/components/bx/base/ApprovalHistoryView.vue'
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
import BxAttachmentReadonly from '@/components/bx/base/BxAttachmentReadonly.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import { bxViewKv, bxViewKvLong } from '@/utils/bx/view-kv'
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
type DialogKey = 'viewDetail' | 'approve'
const showMorePendingFilters = ref(false)
const showMoreApprovedFilters = ref(false)
const listColumnDefs = [
{ prop: 'applyTime', label: '申请时间' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'title', label: '报销标题' },
{ prop: 'expenseName', label: '费用项目' },
{ prop: 'amount', label: '报销金额(元)' },
{ prop: 'status', label: '状态', defaultVisible: false },
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('bx-reimburse-approve-list-columns', listColumnDefs)
const pendingQuickSearchKeyword = ref('')
const approvedQuickSearchKeyword = ref('')
const { applicantOptions, loadApplicantOptions } = useApplicantOptions()
const { expenseTree } = useExpenseItemTree()
const loadingMap = reactive<Record<string, boolean>>({})
@@ -619,14 +651,6 @@ const tabLoaded = reactive({ pending: false, approved: false })
const activeTab = ref('pending')
const menuStore = useMenuStore()
const stats = computed(() => {
const s = reimburseApproverStats.value
return [
{ name: '待审批', value: String(s.pendingTotal) },
{ name: '本月审批金额(元)', value: formatMoney(s.monthPendingAmount) }
]
})
const viewData = ref<Record<string, any>>({})
const viewMainKvFields = computed<ViewField[]>(() => {
@@ -684,7 +708,7 @@ const approvedFilterForm = reactive({
function buildPendingQuery() {
const q: Record<string, string | number> = {}
const code = pendingFilterForm.code.trim()
const code = (pendingQuickSearchKeyword.value.trim() || pendingFilterForm.code.trim())
if (code) q.keyword = code
if (pendingFilterForm.applicantId != null) q.applicantId = pendingFilterForm.applicantId
if (pendingFilterForm.applyDate) q.applyDate = pendingFilterForm.applyDate
@@ -693,7 +717,7 @@ function buildPendingQuery() {
function buildApprovedQuery() {
const q: Record<string, string | number> = {}
const code = approvedFilterForm.code.trim()
const code = (approvedQuickSearchKeyword.value.trim() || approvedFilterForm.code.trim())
if (code) q.keyword = code
if (approvedFilterForm.applicantId != null) q.applicantId = approvedFilterForm.applicantId
if (approvedFilterForm.applyDate) q.applyDate = approvedFilterForm.applyDate
@@ -719,10 +743,12 @@ function handleTabChange(tab: string | number) {
}
function handlePendingSearch() {
pendingQuickSearchKeyword.value = ''
searchPending(buildPendingQuery())
}
function handlePendingReset() {
pendingQuickSearchKeyword.value = ''
Object.assign(pendingFilterForm, {
code: '',
applicantId: undefined,
@@ -734,11 +760,22 @@ function handlePendingReset() {
searchPending()
}
function handlePendingQuickSearch() {
searchPending(buildPendingQuery())
}
function handlePendingQuickSearchClear() {
pendingQuickSearchKeyword.value = ''
searchPending(buildPendingQuery())
}
function handleApprovedSearch() {
approvedQuickSearchKeyword.value = ''
searchApproved(buildApprovedQuery())
}
function handleApprovedReset() {
approvedQuickSearchKeyword.value = ''
Object.assign(approvedFilterForm, {
code: '',
applicantId: undefined,
@@ -750,6 +787,15 @@ function handleApprovedReset() {
searchApproved()
}
function handleApprovedQuickSearch() {
searchApproved(buildApprovedQuery())
}
function handleApprovedQuickSearchClear() {
approvedQuickSearchKeyword.value = ''
searchApproved(buildApprovedQuery())
}
const selectedRows = ref<any[]>([])
function onSelectionChange(rows: any[]) {
@@ -823,21 +869,13 @@ async function handleConfirmApprove() {
}
}
function pendingSummaryMethod(params: { data: any[] }) {
const data = params.data
const n = data.length
const t = data.reduce((s, r) => s + Number(r.amount || 0), 0)
const lbl = `本页合计(${n}单)`
return ['', '', '', lbl, '', '', '', formatSumMoney(t), '', '']
}
function approvedSummaryMethod(params: { data: any[] }) {
const data = params.data
const n = data.length
const t = data.reduce((s, r) => s + Number(r.amount || 0), 0)
const lbl = `本页合计(${n}单)`
return ['', '', '', lbl, '', '', formatSumMoney(t), '', '', '']
}
const amountSummary = buildPageSummaryMethod({
sumByProp: {
amount: (data) => sumField(data, 'amount')
}
})
const pendingSummaryMethod = amountSummary
const approvedSummaryMethod = amountSummary
function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
const sums: string[] = []
+79 -124
View File
@@ -2,13 +2,6 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<div class="stats">
<div v-for="item in stats" :key="item.name" class="stat" :class="{ 'stat-money': item.name.includes('金额') }">
<p class="k">{{ item.name }}</p>
<p class="v">{{ item.value }}</p>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">全部申请单</h2>
@@ -40,45 +33,27 @@
<el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
</el-select>
</el-form-item>
</div>
<div v-show="showMoreFilters" id="all-apply-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="申请时间">
<el-date-picker
v-model="filterForm.applyDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="金额区间(元)">
<el-input v-model="filterForm.amountRange" placeholder="如 1000-5000" clearable />
</el-form-item>
<el-form-item label="当前节点">
<el-select v-model="filterForm.currentNode" placeholder="全部" clearable>
<el-option v-for="n in nodeOptions" :key="n" :label="n" :value="n" />
</el-select>
</el-form-item>
</div>
<el-form-item label="申请时间">
<el-date-picker
v-model="filterForm.applyDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
clearable
style="width: 100%"
/>
</el-form-item>
<el-form-item label="金额区间(元)">
<el-input v-model="filterForm.amountRange" placeholder="如 1000-5000" clearable />
</el-form-item>
<el-form-item label="当前节点">
<el-select v-model="filterForm.currentNode" placeholder="全部" clearable>
<el-option v-for="n in nodeOptions" :key="n" :label="n" :value="n" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="showMoreFilters"
aria-controls="all-apply-filter-extra"
@click="toggleMoreFilters"
>
{{ showMoreFilters ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="showMoreFilters" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -93,6 +68,12 @@
<div class="list-toolbar">
<div class="list-toolbar-row">
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="单号/标题/申请人"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
@@ -106,7 +87,7 @@
</div>
<div class="list-body list-wrap">
<div class="table-scroll table-page-summary">
<div class="table-scroll table-page-summary table-page-summary-biz-split">
<el-table
class="list-table"
:data="pager.pagedRows.value"
@@ -115,7 +96,10 @@
show-summary
:summary-method="summaryMethod"
:max-height="'min(520px, calc(100vh - 300px))'"
@row-dblclick="openViewDetail"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
@@ -129,6 +113,7 @@
<el-table-column
prop="applyCode"
label="申请单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -175,7 +160,7 @@
label="申请金额(元)"
:formatter="moneyTableFormatter"
class-name="col-money"
min-width="120"
min-width="240"
align="right"
header-align="center"
/>
@@ -259,41 +244,28 @@
<h4 class="dlg-view-section-title">基础信息</h4>
<BxViewKvTable :fields="viewDetailKvFields" mode="triple" />
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="viewFlowNodes" :loading="viewFlowLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="viewApprovalHistory" :loading="viewFlowLoading" />
</div>
</div>
<BxApprovalFlowTabs
:nodes="viewFlowNodes"
:history="viewApprovalHistory"
:loading="viewFlowLoading"
/>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.viewDetail = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Close, Search, RefreshLeft, ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
import { Close, Search, RefreshLeft } from '@element-plus/icons-vue'
import { advanceApi, reimburseApi } from '@/api/bx'
import {
bizTypeTagType,
statusLabel,
statusTagType,
formatSumMoney,
formatMoneyYuan,
moneyTableFormatter,
workflowNodeDisplay,
@@ -301,18 +273,18 @@ import {
workflowNodeTagType,
shouldShowWorkflowNodeTag
} from '@/utils/bx/format'
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
import ApprovalHistoryView from '@/components/bx/base/ApprovalHistoryView.vue'
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import { bxViewKv, bxViewKvMoney } from '@/utils/bx/view-kv'
import { buildPageSummaryMethod, sumFieldByBizType } from '@/utils/bx/table-summary'
import { usePagedTable } from '@/hooks/bx/usePagedTable'
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
type DialogKey = 'viewDetail'
const showMoreFilters = ref(false)
const loading = ref(false)
const selectedRows = ref<any[]>([])
const dialogs = reactive<Record<DialogKey, boolean>>({
viewDetail: false
})
@@ -338,6 +310,8 @@ const {
resetToDefault
} = usePersistedListColumns('bx-view-all-list-columns', listColumnDefs)
const quickSearchKeyword = ref('')
const filterForm = reactive({
code: '',
bizType: '' as string,
@@ -350,6 +324,21 @@ const filterForm = reactive({
function filterMergedRows(rows: any[]) {
let list = [...rows]
const quickKw = quickSearchKeyword.value.trim().toLowerCase()
if (quickKw) {
list = list.filter(
(r: any) =>
String(r.applyCode || '')
.toLowerCase()
.includes(quickKw) ||
String(r.title || '')
.toLowerCase()
.includes(quickKw) ||
String(r.applicantName || '')
.toLowerCase()
.includes(quickKw)
)
}
const code = filterForm.code.trim()
if (code) list = list.filter((r: any) => String(r.applyCode || '').includes(code))
if (filterForm.bizType && filterForm.bizType !== '全部') {
@@ -415,35 +404,16 @@ const viewFlowNodes = ref<any[]>([])
const viewApprovalHistory = ref<any[]>([])
const viewFlowLoading = ref(false)
const stats = ref([
{ name: '全部单据', value: '-' },
{ name: '审批中', value: '-' },
{ name: '已通过', value: '-' },
{ name: '已发放', value: '-' },
{ name: '已驳回', value: '-' }
])
const bizTypeOptions = ['全部', '报销单', '备用金']
const statusOptions = ['全部', '待提交', '审批中', '审批通过', '审批驳回', '已发放']
const nodeOptions = ['全部', '草稿', '发起人主管审批', '财务复核', '总经理审批', '已完成', '已驳回', '已发放']
function countStatus(m: Record<string, number> | undefined, key: string) {
return Number(m?.[key] ?? 0)
}
function sumStatusMap(m: Record<string, number> | undefined) {
if (!m) return 0
return Object.values(m).reduce((acc, v) => acc + Number(v || 0), 0)
}
async function loadData() {
loading.value = true
try {
const [advRes, reimRes, advCntRes, reimCntRes]: any[] = await Promise.all([
const [advRes, reimRes]: any[] = await Promise.all([
advanceApi.list({ page: 1, pageSize: 999 }),
reimburseApi.list({ page: 1, pageSize: 999 }),
advanceApi.statusCounts(),
reimburseApi.statusCounts()
reimburseApi.list({ page: 1, pageSize: 999 })
])
const advRows = (advRes.data?.rows || []).map((r: any) => ({
...r,
@@ -462,24 +432,6 @@ async function loadData() {
applicantId: r.applicantId
}))
mergedRows.value = [...advRows, ...reimRows]
const ac = advCntRes.data as Record<string, number> | undefined
const rc = reimCntRes.data as Record<string, number> | undefined
const totalAll = sumStatusMap(rc) + sumStatusMap(ac)
const pending = countStatus(rc, 'pending') + countStatus(ac, 'pending')
const approved = countStatus(rc, 'approved') + countStatus(ac, 'approved')
const issued =
countStatus(rc, 'processed') +
countStatus(rc, 'paid') +
countStatus(rc, 'offset') +
countStatus(ac, 'issued')
const rejected = countStatus(rc, 'rejected') + countStatus(ac, 'rejected')
stats.value = [
{ name: '全部单据', value: String(totalAll) },
{ name: '审批中', value: String(pending) },
{ name: '已通过', value: String(approved) },
{ name: '已发放', value: String(issued) },
{ name: '已驳回', value: String(rejected) }
]
} catch {
// error handled by interceptor
} finally {
@@ -487,15 +439,13 @@ async function loadData() {
}
}
function toggleMoreFilters() {
showMoreFilters.value = !showMoreFilters.value
}
function handleSearch() {
quickSearchKeyword.value = ''
pager.currentPage.value = 1
}
function handleReset() {
quickSearchKeyword.value = ''
Object.assign(filterForm, {
code: '',
bizType: '',
@@ -508,18 +458,23 @@ function handleReset() {
pager.currentPage.value = 1
}
function summaryMethod(params: { columns: { property?: string; type?: string }[]; data: Array<{ amount?: number }> }) {
const totalAmount = params.data.reduce((sum, row) => sum + Number(row.amount || 0), 0)
let labelPlaced = false
return params.columns.map((col) => {
if (col.type === 'index') return ''
if (!labelPlaced) {
labelPlaced = true
return `本页合计(${params.data.length}单)`
}
if (col.property === 'amount') return formatSumMoney(totalAmount)
return ''
})
function handleQuickSearch() {
pager.currentPage.value = 1
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pager.currentPage.value = 1
}
const summaryMethod = buildPageSummaryMethod({
sumByProp: {
amount: (data) => sumFieldByBizType(data, 'amount')
}
})
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows
}
async function openViewDetail(row: any) {
+63 -80
View File
@@ -2,13 +2,6 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<div class="stats">
<div v-for="item in stats" :key="item.name" class="stat" :class="{ 'stat-money': item.name.includes('金额') }">
<p class="k">{{ item.name }}</p>
<p class="v">{{ item.value }}</p>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">我的申请单</h2>
@@ -47,30 +40,12 @@
style="width: 100%"
/>
</el-form-item>
</div>
<div v-show="showMoreFilters" id="mine-apply-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="金额区间(元)">
<el-input v-model="filterForm.amountRange" placeholder="如 1000-5000" clearable />
</el-form-item>
</div>
<el-form-item label="金额区间(元)">
<el-input v-model="filterForm.amountRange" placeholder="如 1000-5000" clearable />
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="showMoreFilters"
aria-controls="mine-apply-filter-extra"
@click="toggleMoreFilters"
>
{{ showMoreFilters ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="showMoreFilters" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button native-type="button" class="btn-ghost" :loading="searchLoading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -85,6 +60,12 @@
<div class="list-toolbar">
<div class="list-toolbar-row">
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="单号/申请标题"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<ListToolbarEndActions
:loading="tableLoading"
:column-defs="listColumnDefs"
@@ -98,14 +79,19 @@
</div>
<div class="list-body list-wrap">
<div class="table-scroll">
<div class="table-scroll table-page-summary table-page-summary-biz-split">
<el-table
class="list-table"
:data="pagedRows"
v-loading="tableLoading"
stripe
show-summary
:summary-method="summaryMethod"
:max-height="'min(520px, calc(100vh - 300px))'"
@row-dblclick="openViewDetail"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
@@ -157,7 +143,7 @@
label="申请金额(元)"
:formatter="moneyTableFormatter"
class-name="col-money"
min-width="120"
min-width="240"
align="right"
header-align="center"
/>
@@ -240,35 +226,23 @@
<h4 class="dlg-view-section-title">基础信息</h4>
<BxViewKvTable :fields="viewDetailKvFields" mode="triple" />
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="viewFlowNodes" :loading="viewFlowLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="viewApprovalHistory" :loading="viewFlowLoading" />
</div>
</div>
<BxApprovalFlowTabs
:nodes="viewFlowNodes"
:history="viewApprovalHistory"
:loading="viewFlowLoading"
/>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.viewDetail = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { computed, onActivated, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Close, Search, RefreshLeft, ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
import { Close, Search, RefreshLeft } from '@element-plus/icons-vue'
import { advanceApi, reimburseApi } from '@/api/bx'
import {
bizTypeTagType,
@@ -281,18 +255,18 @@ import {
workflowNodeTagType,
shouldShowWorkflowNodeTag
} from '@/utils/bx/format'
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
import ApprovalHistoryView from '@/components/bx/base/ApprovalHistoryView.vue'
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import { bxViewKv, bxViewKvMoney } from '@/utils/bx/view-kv'
import { buildPageSummaryMethod, sumFieldByBizType } from '@/utils/bx/table-summary'
import { usePagedTable } from '@/hooks/bx/usePagedTable'
type DialogKey = 'viewDetail'
const showMoreFilters = ref(false)
const tableLoading = ref(false)
const searchLoading = ref(false)
const selectedRows = ref<any[]>([])
const dialogs = reactive<Record<DialogKey, boolean>>({
viewDetail: false
})
@@ -316,6 +290,8 @@ const {
resetToDefault
} = usePersistedListColumns('bx-view-mine-list-columns', listColumnDefs)
const quickSearchKeyword = ref('')
const filterForm = reactive({
code: '',
bizType: '' as string,
@@ -338,6 +314,13 @@ function syncAppliedFilter() {
function filterMineRows(rows: any[]) {
let list = [...rows]
const kw = appliedFilter.code.trim()
if (kw) {
list = list.filter(
(r: any) =>
String(r.applyCode || '').includes(kw) || String(r.title || '').includes(kw)
)
}
if (appliedFilter.bizType) {
list = list.filter((r: any) => r.bizType === appliedFilter.bizType)
}
@@ -383,13 +366,6 @@ const viewFlowNodes = ref<any[]>([])
const viewApprovalHistory = ref<any[]>([])
const viewFlowLoading = ref(false)
const stats = ref([
{ name: '全部申请', value: '-' },
{ name: '审批中', value: '-' },
{ name: '已通过', value: '-' },
{ name: '已驳回', value: '-' }
])
function buildApiQuery() {
const q: Record<string, string> = {}
const code = appliedFilter.code.trim()
@@ -421,17 +397,21 @@ function sortMergedRows(rows: any[]) {
return [...rows].sort((a, b) => String(b.applyTime || '').localeCompare(String(a.applyTime || '')))
}
const summaryMethod = buildPageSummaryMethod({
sumByProp: {
amount: (data) => sumFieldByBizType(data, 'amount')
}
})
async function loadData(options: { buttonLoading?: boolean } = {}) {
if (options.buttonLoading) searchLoading.value = true
tableLoading.value = true
try {
const apiQuery = buildApiQuery()
const listParams = { page: 1, pageSize: 9999, ...apiQuery }
const [advRes, reimRes, advSt, reimSt] = await Promise.allSettled([
const [advRes, reimRes] = await Promise.allSettled([
advanceApi.my(listParams),
reimburseApi.my(listParams),
advanceApi.myStats(),
reimburseApi.myStats()
reimburseApi.my(listParams)
])
const advRows =
advRes.status === 'fulfilled'
@@ -442,19 +422,6 @@ async function loadData(options: { buttonLoading?: boolean } = {}) {
? (reimRes.value.data?.rows || []).map(mapReimburseMineRow)
: []
mergedRows.value = sortMergedRows([...advRows, ...reimRows])
const a = advSt.status === 'fulfilled' ? advSt.value.data || {} : {}
const r = reimSt.status === 'fulfilled' ? reimSt.value.data || {} : {}
const totalCount = Number(r.totalCount ?? 0) + Number(a.totalCount ?? 0)
const pending = Number(r.pendingCount ?? 0) + Number(a.pendingCount ?? 0)
const approved =
Number(r.approvedCount ?? 0) + Number(a.approvedPassCount ?? 0) + Number(a.issuedCount ?? 0)
const rejected = Number(r.rejectedCount ?? 0) + Number(a.rejectedCount ?? 0)
stats.value = [
{ name: '全部申请', value: String(totalCount) },
{ name: '审批中', value: String(pending) },
{ name: '已通过', value: String(approved) },
{ name: '已驳回', value: String(rejected) }
]
} catch {
// error handled by interceptor
} finally {
@@ -463,23 +430,39 @@ async function loadData(options: { buttonLoading?: boolean } = {}) {
}
}
function toggleMoreFilters() {
showMoreFilters.value = !showMoreFilters.value
}
function handleSearch() {
quickSearchKeyword.value = ''
syncAppliedFilter()
currentPage.value = 1
loadData({ buttonLoading: true })
}
function handleReset() {
quickSearchKeyword.value = ''
Object.assign(filterForm, { code: '', bizType: '', status: '', applyDate: null, amountRange: '' })
Object.assign(appliedFilter, { code: '', bizType: '', status: '', applyDate: null, amountRange: '' })
currentPage.value = 1
loadData({ buttonLoading: true })
}
function handleQuickSearch() {
appliedFilter.code = quickSearchKeyword.value.trim()
currentPage.value = 1
loadData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
appliedFilter.code = ''
currentPage.value = 1
loadData()
}
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows
}
async function openViewDetail(row: any) {
currentRow.value = row
viewFlowNodes.value = []
+105 -84
View File
@@ -2,13 +2,6 @@
<!-- eslint-disable vue/no-v-model-argument -->
<template>
<div class="page-container">
<div class="stats">
<div v-for="item in stats" :key="item.name" class="stat" :class="{ 'stat-money': item.name.includes('金额') }">
<p class="k">{{ item.name }}</p>
<p class="v">{{ item.value }}</p>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">待我审批</h2>
@@ -46,30 +39,12 @@
style="width: 100%"
/>
</el-form-item>
</div>
<div v-show="showMoreFilters" id="todo-approve-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="金额区间(元)">
<el-input v-model="filterForm.amountRange" placeholder="如 1000-5000" clearable />
</el-form-item>
</div>
<el-form-item label="金额区间(元)">
<el-input v-model="filterForm.amountRange" placeholder="如 1000-5000" clearable />
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="showMoreFilters"
aria-controls="todo-approve-filter-extra"
@click="toggleMoreFilters"
>
{{ showMoreFilters ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="showMoreFilters" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button native-type="button" class="btn-ghost" :loading="searchLoading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -87,6 +62,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="单号/标题/申请人"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" :loading="loadingMap.batchPass" @click="handleBatchPass">
<el-icon><Check /></el-icon>批量通过
</el-button>
@@ -94,7 +75,15 @@
<el-icon><Close /></el-icon>批量驳回
</el-button>
<ListToolbarEndActions :loading="tableLoading" @refresh="loadData" />
<ListToolbarEndActions
:loading="tableLoading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="loadData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -114,6 +103,7 @@
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applyTime')"
prop="applyTime"
label="申请时间"
min-width="160"
@@ -124,6 +114,7 @@
<el-table-column
prop="applyCode"
label="申请单号"
class-name="col-apply-code"
min-width="140"
align="center"
header-align="center"
@@ -133,15 +124,30 @@
<el-link type="primary" @click="openViewDetail(row)">{{ row.applyCode }}</el-link>
</template>
</el-table-column>
<el-table-column prop="bizType" label="业务类型" min-width="100" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('bizType')"
prop="bizType"
label="业务类型"
min-width="100"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="bizTypeTagType(row.bizType)" size="small">
{{ row.bizType }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="applicantName" label="申请人" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('title')"
prop="title"
label="申请标题"
min-width="180"
@@ -150,6 +156,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('amount')"
prop="amount"
label="申请金额(元)"
:formatter="moneyTableFormatter"
@@ -158,7 +165,14 @@
align="right"
header-align="center"
/>
<el-table-column prop="currentNode" label="当前节点" min-width="120" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('currentNode')"
prop="currentNode"
label="当前节点"
min-width="120"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag
v-if="shouldShowWorkflowNodeTag(row.currentNode, row.status)"
@@ -234,25 +248,12 @@
<h4 class="dlg-view-section-title">基础信息</h4>
<BxViewKvTable :fields="viewDetailKvFields" mode="triple" />
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批进度</h4>
<div class="flow-scroll">
<ApprovalFlowView :nodes="viewFlowNodes" :loading="viewFlowLoading" />
</div>
</div>
<div class="dlg-view-section">
<h4 class="dlg-view-section-title">审批历史</h4>
<div class="flow-scroll">
<ApprovalHistoryView :items="viewApprovalHistory" :loading="viewFlowLoading" />
</div>
</div>
<BxApprovalFlowTabs
:nodes="viewFlowNodes"
:history="viewApprovalHistory"
:loading="viewFlowLoading"
/>
</div>
<template #footer>
<el-button class="btn-ghost" @click="dialogs.viewDetail = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
<el-dialog
@@ -264,7 +265,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top">
<el-form label-position="right" label-width="100px" class="bx-dialog-form">
<el-form-item label="审批意见">
<el-input v-model="approveOpinion" type="textarea" :rows="4" placeholder="请输入审批意见" />
</el-form-item>
@@ -286,20 +287,19 @@
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { computed, onActivated, onMounted, reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import {
Check,
Close,
Search,
RefreshLeft,
ArrowUpBold,
ArrowDownBold
RefreshLeft
} from '@element-plus/icons-vue'
import { advanceApi, reimburseApi } from '@/api/bx'
import {
bizTypeTagType,
formatSumMoney,
formatMoneyYuan,
moneyTableFormatter,
workflowNodeDisplay,
@@ -307,17 +307,16 @@ import {
workflowNodeLabel,
workflowNodeTagType
} from '@/utils/bx/format'
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
import ApprovalHistoryView from '@/components/bx/base/ApprovalHistoryView.vue'
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
import BxViewKvTable from '@/components/bx/base/BxViewKvTable.vue'
import type { ViewField } from '@/components/business/system-view-dialog/types'
import { bxViewKv, bxViewKvMoney } from '@/utils/bx/view-kv'
import { buildPageSummaryMethod, sumField } from '@/utils/bx/table-summary'
import { usePagedTable } from '@/hooks/bx/usePagedTable'
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
type DialogKey = 'viewDetail' | 'approve'
const showMoreFilters = ref(false)
const tableLoading = ref(false)
const searchLoading = ref(false)
const loadingMap = reactive<Record<string, boolean>>({})
@@ -329,6 +328,25 @@ const dialogs = reactive<Record<DialogKey, boolean>>({
const mergedRows = ref<any[]>([])
const { applicantOptions, loadApplicantOptions } = useApplicantOptions()
const quickSearchKeyword = ref('')
const listColumnDefs = [
{ prop: 'applyTime', label: '申请时间' },
{ prop: 'bizType', label: '业务类型' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'title', label: '申请标题' },
{ prop: 'amount', label: '申请金额(元)' },
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('bx-view-todo-list-columns', listColumnDefs)
const filterForm = reactive({
code: '',
bizType: '' as string,
@@ -351,6 +369,15 @@ function syncAppliedFilter() {
function filterTodoRows(rows: any[]) {
let list = [...rows]
const kw = appliedFilter.code.trim()
if (kw) {
list = list.filter(
(r: any) =>
String(r.applyCode || '').includes(kw) ||
String(r.title || '').includes(kw) ||
String(r.applicantName || '').includes(kw)
)
}
if (appliedFilter.bizType) {
list = list.filter((r: any) => r.bizType === appliedFilter.bizType)
}
@@ -397,14 +424,6 @@ const viewFlowNodes = ref<any[]>([])
const viewApprovalHistory = ref<any[]>([])
const viewFlowLoading = ref(false)
const stats = ref([
{ name: '待审批报销', value: '-' },
{ name: '待审批备用金', value: '-' },
{ name: '待审批合计', value: '-' },
{ name: '紧急单据', value: '0' }
])
function buildApiQuery() {
const q: Record<string, string | number> = {}
const code = appliedFilter.code.trim()
@@ -452,16 +471,6 @@ async function loadData(options: { buttonLoading?: boolean } = {}) {
const reimRows =
reimRes.status === 'fulfilled' ? (reimRes.value.data?.rows || []).map(mapReimburseTodoRow) : []
mergedRows.value = [...advRows, ...reimRows]
const reimTotal =
reimRes.status === 'fulfilled' ? Number(reimRes.value.data?.total ?? reimRows.length) : 0
const advTotal =
advRes.status === 'fulfilled' ? Number(advRes.value.data?.total ?? advRows.length) : 0
stats.value = [
{ name: '待审批报销', value: String(reimTotal) },
{ name: '待审批备用金', value: String(advTotal) },
{ name: '待审批合计', value: String(reimTotal + advTotal) },
{ name: '紧急单据', value: '0' }
]
} catch {
// error handled by interceptor
} finally {
@@ -523,10 +532,6 @@ async function handleDialogReject() {
}
}
function toggleMoreFilters() {
showMoreFilters.value = !showMoreFilters.value
}
const selectedRows = ref<any[]>([])
function onSelectionChange(rows: any[]) {
@@ -574,12 +579,14 @@ async function handleBatchReject() {
}
function handleSearch() {
quickSearchKeyword.value = ''
syncAppliedFilter()
currentPage.value = 1
loadData({ buttonLoading: true })
}
function handleReset() {
quickSearchKeyword.value = ''
Object.assign(filterForm, {
code: '',
bizType: '',
@@ -598,11 +605,25 @@ function handleReset() {
loadData({ buttonLoading: true })
}
function summaryMethod(params: { data: Array<{ amount?: number }> }) {
const totalAmount = params.data.reduce((sum, row) => sum + Number(row.amount || 0), 0)
return ['', '', `本页合计(${params.data.length}单)`, '', '', '', '', formatSumMoney(totalAmount), '', '']
function handleQuickSearch() {
appliedFilter.code = quickSearchKeyword.value.trim()
currentPage.value = 1
loadData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
appliedFilter.code = ''
currentPage.value = 1
loadData()
}
const summaryMethod = buildPageSummaryMethod({
sumByProp: {
amount: (data) => sumField(data, 'amount')
}
})
function openViewDetailFromRow(row: any) {
openViewDetail(row)
}
@@ -22,6 +22,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="节点名称/审批人"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="openAddDialog">
<el-icon><Plus /></el-icon>新增节点
</el-button>
@@ -52,17 +58,26 @@
<el-icon><Upload /></el-icon>保存配置
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="loadWfDef" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="loadWfDef"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="nodes"
:data="filteredNodes"
v-loading="loading"
stripe
@selection-change="onSelectionChange"
@row-dblclick="openEditDialog"
>
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
@@ -74,8 +89,16 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column prop="nodeOrder" label="节点顺序" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('nodeOrder')"
prop="nodeOrder"
label="节点顺序"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('approverName')"
prop="approverName"
label="审批人"
min-width="120"
@@ -83,7 +106,13 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="配置状态" min-width="110" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('configStatus')"
label="配置状态"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="(row.approverId ?? row.approver_id) ? 'success' : 'warning'" size="small">
{{ (row.approverId ?? row.approver_id) ? '已配置' : '未配置' }}
@@ -107,7 +136,9 @@
ref="addFormRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="bx-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -155,7 +186,9 @@
ref="editFormRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="bx-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -195,6 +228,8 @@
<script setup lang="ts">
import { reactive, ref, computed, onMounted } from 'vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance } from 'element-plus'
@@ -220,10 +255,39 @@ const dialogs = reactive<Record<DialogKey, boolean>>({
editNode: false
})
const quickSearchKeyword = ref('')
const listColumnDefs = [
{ prop: 'nodeOrder', label: '节点顺序' },
{ prop: 'approverName', label: '审批人' },
{ prop: 'configStatus', label: '配置状态' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('bx-workflow-advance-list-columns', listColumnDefs)
const loading = ref(false)
const selectedRows = ref<Record<string, any>[]>([])
const wfDef = ref<Record<string, any>>({})
const nodes = computed(() => wfDef.value.nodes || [])
const allNodes = computed(() => wfDef.value.nodes || [])
const filteredNodes = computed(() => {
const kw = quickSearchKeyword.value.trim().toLowerCase()
if (!kw) return allNodes.value
return allNodes.value.filter(
(n: any) =>
String(n.nodeName || '')
.toLowerCase()
.includes(kw) ||
String(n.approverName || '')
.toLowerCase()
.includes(kw)
)
})
const nodes = allNodes
const userOptions = ref<
{ id: string; realName: string; username: string; roles: string; displayName: string }[]
>([])
@@ -283,6 +347,14 @@ function onSelectionChange(rows: Record<string, any>[]) {
selectedRows.value = rows
}
function handleQuickSearch() {
/* filteredNodes reacts to quickSearchKeyword */
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
}
function handleToolbarEdit() {
if (selectedRows.value.length !== 1) {
ElMessage.warning('请先勾选一条节点')
@@ -22,6 +22,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="节点名称/审批人"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="openAddDialog">
<el-icon><Plus /></el-icon>新增节点
</el-button>
@@ -52,17 +58,26 @@
<el-icon><Upload /></el-icon>保存配置
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="loadWfDef" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="loadWfDef"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="nodes"
:data="filteredNodes"
v-loading="loading"
stripe
@selection-change="onSelectionChange"
@row-dblclick="openEditDialog"
>
<el-table-column type="selection" min-width="48" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
@@ -74,8 +89,16 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column prop="nodeOrder" label="节点顺序" min-width="100" align="center" header-align="center" />
<el-table-column
v-if="isColumnVisible('nodeOrder')"
prop="nodeOrder"
label="节点顺序"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('approverName')"
prop="approverName"
label="审批人"
min-width="120"
@@ -83,7 +106,13 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="配置状态" min-width="110" align="center" header-align="center">
<el-table-column
v-if="isColumnVisible('configStatus')"
label="配置状态"
min-width="110"
align="center"
header-align="center"
>
<template #default="{ row }">
<el-tag :type="(row.approverId ?? row.approver_id) ? 'success' : 'warning'" size="small">
{{ (row.approverId ?? row.approver_id) ? '已配置' : '未配置' }}
@@ -107,7 +136,9 @@
ref="addFormRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="bx-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -155,7 +186,9 @@
ref="editFormRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="bx-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -195,6 +228,8 @@
<script setup lang="ts">
import { reactive, ref, computed, onMounted } from 'vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance } from 'element-plus'
@@ -220,10 +255,39 @@ const dialogs = reactive<Record<DialogKey, boolean>>({
editNode: false
})
const quickSearchKeyword = ref('')
const listColumnDefs = [
{ prop: 'nodeOrder', label: '节点顺序' },
{ prop: 'approverName', label: '审批人' },
{ prop: 'configStatus', label: '配置状态' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('bx-workflow-reimburse-list-columns', listColumnDefs)
const loading = ref(false)
const selectedRows = ref<Record<string, any>[]>([])
const wfDef = ref<Record<string, any>>({})
const nodes = computed(() => wfDef.value.nodes || [])
const allNodes = computed(() => wfDef.value.nodes || [])
const filteredNodes = computed(() => {
const kw = quickSearchKeyword.value.trim().toLowerCase()
if (!kw) return allNodes.value
return allNodes.value.filter(
(n: any) =>
String(n.nodeName || '')
.toLowerCase()
.includes(kw) ||
String(n.approverName || '')
.toLowerCase()
.includes(kw)
)
})
const nodes = allNodes
const userOptions = ref<
{ id: string; realName: string; username: string; roles: string; displayName: string }[]
>([])
@@ -283,6 +347,14 @@ function onSelectionChange(rows: Record<string, any>[]) {
selectedRows.value = rows
}
function handleQuickSearch() {
/* filteredNodes reacts to quickSearchKeyword */
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
}
function handleToolbarEdit() {
if (selectedRows.value.length !== 1) {
ElMessage.warning('请先勾选一条节点')
+3 -1
View File
@@ -17,7 +17,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
+3 -1
View File
@@ -39,7 +39,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
+68 -62
View File
@@ -15,7 +15,7 @@
<el-button
v-if="hasCluePerm('edit')"
class="btn-ghost"
:disabled="clueInfo.isConverted === 1"
:disabled="clueInfo.isConverted === 1 || clueInfo.status === 'invalid'"
@click="handleEdit"
>
<el-icon><Edit /></el-icon>修改
@@ -141,10 +141,8 @@
import { computed, ref } from 'vue'
import { User, Edit, Delete, Plus, Refresh, CircleClose } from '@element-plus/icons-vue'
import { useUserStore } from '@/store/modules/user'
import SystemViewDialog, {
type ViewField,
type ViewSection
} from '@/components/business/system-view-dialog/index.vue'
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { crmDictField, crmField, crmLongField } from '@/utils/crm/view-fields'
import { getClue, listClueFollow, delClueFollow, delClue } from '@/api/crm/clue'
import type { ClueVO, ClueFollowVO, ClueFollowQuery } from '@/api/crm/clue/types'
import { useDictStore } from '@/store/modules/dict'
@@ -200,29 +198,6 @@
return parts.join(' · ')
})
function tagField(
label: string,
dictType: string,
value?: string | null,
extra?: Partial<ViewField>
): ViewField {
return {
label,
value: value ? dictStore.getLabel(dictType, value) : '—',
tagType: value ? dictStore.getTagType(dictType, value) : undefined,
...extra
}
}
function viewTableSection(title: string, fields: ViewField[]): ViewSection {
return {
title,
fields,
fieldLayout: 'table',
tableMode: 'dual'
}
}
function parseClueRemark(remark?: string | null) {
if (!remark?.trim()) return { remarkText: '', invalidReason: '' }
const marker = '无效原因:'
@@ -240,60 +215,68 @@
const address = [c.province, c.city, c.district, c.address].filter(Boolean).join(' ')
const basicFields: ViewField[] = [
{ label: '公司名称', value: c.company },
{ label: '手机号', value: c.mobile },
{ label: '职位', value: c.position },
{ label: '邮箱', value: c.email },
tagField('客户类型', 'crm_customer_type', c.customerType),
{ label: '客户网站', value: c.website, span: true },
tagField('所属行业', 'crm_customer_industry', c.industry),
tagField('所在园区', 'crm_customer_park', c.parkArea),
{ label: '地址', value: address, span: true }
const basicFields = [
crmField('公司名称', c.company),
crmField('手机号', c.mobile),
crmField('职位', c.position),
crmField('邮箱', c.email),
crmDictField('客户类型', 'crm_customer_type', c.customerType, dictStore),
crmField('客户网站', c.website, { long: false }),
crmDictField('所属行业', 'crm_customer_industry', c.industry, dictStore),
crmDictField('所在园区', 'crm_customer_park', c.parkArea, dictStore),
crmLongField('地址', address)
]
const clueFields: ViewField[] = [
tagField('线索等级', 'crm_clue_level', c.level),
tagField('线索来源', 'crm_clue_source', c.source),
tagField('线索状态', 'crm_clue_status', c.status),
{ label: '负责人', value: c.ownerUserName },
{ label: '下次联系时间', value: c.nextTime },
{ label: '最后跟进时间', value: c.lastTime },
{ label: '跟进次数', value: `${c.followCount ?? 0} 次` },
{ label: '创建人', value: (c as any).createByName || (c as any).createBy },
{ label: '创建时间', value: c.createTime }
const clueFields = [
crmDictField('线索等级', 'crm_clue_level', c.level, dictStore),
crmDictField('线索来源', 'crm_clue_source', c.source, dictStore),
crmDictField('线索状态', 'crm_clue_status', c.status, dictStore),
crmField('负责人', c.ownerUserName),
crmField('下次联系时间', c.nextTime, { long: false }),
crmField('最后跟进时间', c.lastTime, { long: false }),
crmField('跟进次数', `${c.followCount ?? 0} 次`, { span: 'full' }),
crmField('创建人', (c as any).createByName || (c as any).createBy),
crmField('创建时间', c.createTime, { long: false })
]
if (c.isConverted === 1) {
clueFields.push({ label: '转化时间', value: c.convertedTime })
clueFields.push(crmField('转化时间', c.convertedTime))
}
const sections: ViewSection[] = [
viewTableSection('基本信息', basicFields),
viewTableSection('线索信息', clueFields)
{ title: '基本信息', fields: basicFields, fieldLayout: 'table', tableMode: 'dual' },
{ title: '线索信息', fields: clueFields, fieldLayout: 'table', tableMode: 'dual' }
]
if (c.intentProducts || c.requirement) {
sections.push(
viewTableSection('需求信息', [
{ label: '意向产品', value: c.intentProducts },
{ label: '需求描述', value: c.requirement }
])
)
sections.push({
title: '需求信息',
fieldLayout: 'table',
tableMode: 'dual',
fields: [
crmField('意向产品', c.intentProducts, { long: false, fullSpan: false }),
crmField('需求描述', c.requirement, { long: false, fullSpan: false })
]
})
}
const { remarkText, invalidReason } = parseClueRemark(c.remark)
if (remarkText || invalidReason) {
const remarkFields: ViewField[] = []
const remarkFields = []
if (remarkText) {
remarkFields.push({ label: '备注', value: remarkText })
remarkFields.push(crmLongField('备注', remarkText, { fullSpan: true }))
}
if (invalidReason) {
remarkFields.push({ label: '无效原因', value: invalidReason })
remarkFields.push(crmLongField('无效原因', invalidReason, { fullSpan: true }))
}
sections.push(viewTableSection('备注信息', remarkFields))
sections.push({ title: '备注信息', fields: remarkFields, fieldLayout: 'table', tableMode: 'dual' })
} else if (c.remark) {
sections.push(viewTableSection('备注信息', [{ label: '备注', value: c.remark }]))
sections.push({
title: '备注信息',
fieldLayout: 'table',
tableMode: 'dual',
fields: [crmLongField('备注', c.remark, { fullSpan: true })]
})
}
return sections
@@ -525,5 +508,28 @@
.view-split-right .clue-view-follow-section {
padding: 0;
}
/* 左侧信息区:两列表格模式(dual = 一行两组标签/值) */
.view-split-left .view-kv-table-wrap {
border-color: #eef4f1;
border-radius: 8px;
}
.view-split-left .view-kv-table th {
background: #f8fafc;
font-size: 12px;
padding: 8px 10px;
}
.view-split-left .view-kv-table td {
font-size: 13px;
padding: 8px 12px;
}
.view-split-left .view-kv-table tr.row-long td {
font-weight: 400;
color: #475569;
line-height: 1.55;
}
}
</style>
+3 -1
View File
@@ -13,7 +13,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
+4 -2
View File
@@ -13,7 +13,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -34,7 +36,7 @@
<el-form-item label="公司名称" prop="company">
<el-input v-model="form.company" placeholder="请输入公司名称" maxlength="200" />
</el-form-item>
<el-form-item label="客户类型" prop="customerType" class="form-field-span-2">
<el-form-item label="客户类型" prop="customerType">
<el-select v-model="form.customerType" placeholder="请选择客户类型" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_customer_type')"
+62 -96
View File
@@ -8,7 +8,7 @@
<el-tabs
v-model="activeView"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleViewChange"
>
<el-tab-pane label="我的线索" name="mine" />
@@ -55,78 +55,60 @@
/>
</el-select>
</el-form-item>
</div>
<div v-show="filterExpanded" id="clue-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="线索等级">
<el-select v-model="searchForm.level" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_clue_level')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="线索状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_clue_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="所属行业">
<el-select v-model="searchForm.industry" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_customer_industry')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="所在园区">
<el-select v-model="searchForm.parkArea" placeholder="全部" clearable filterable>
<el-option
v-for="opt in dictStore.getOptions('crm_customer_park')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</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 label="线索等级">
<el-select v-model="searchForm.level" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_clue_level')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-form-item>
</div>
</el-select>
</el-form-item>
<el-form-item label="线索状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_clue_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="所属行业">
<el-select v-model="searchForm.industry" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_customer_industry')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="所在园区">
<el-select v-model="searchForm.parkArea" placeholder="全部" clearable filterable>
<el-option
v-for="opt in dictStore.getOptions('crm_customer_park')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</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-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="clue-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -144,25 +126,23 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-input
<ListQuickSearchInput
v-model="quickSearchKeyword"
class="quick-search-input"
placeholder="姓名/公司/手机号"
clearable
@keyup.enter="handleQuickSearch"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
>
<template #suffix>
<el-icon class="quick-search-icon" @click="handleQuickSearch"><Search /></el-icon>
</template>
</el-input>
/>
<el-button v-if="hasCluePerm('add')" class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增线索
</el-button>
<el-button
v-if="hasCluePerm('edit')"
class="btn-ghost"
:disabled="single || selectedRows[0]?.isConverted === 1"
:disabled="
single ||
selectedRows[0]?.isConverted === 1 ||
selectedRows[0]?.status === 'invalid'
"
@click="handleEdit()"
>
<el-icon><Edit /></el-icon>编辑
@@ -454,7 +434,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top">
<el-form label-position="right" label-width="100px" class="crm-dialog-form">
<el-form-item label="导入模板">
<el-button class="btn-ghost" :loading="templateLoading" @click="downloadTemplate">
<el-icon><Download /></el-icon>下载模板
@@ -504,8 +484,6 @@
Search,
RefreshLeft,
User,
ArrowUpBold,
ArrowDownBold,
Edit,
ChatLineRound,
Refresh,
@@ -534,6 +512,7 @@
import InvalidForm from './invalidForm.vue'
import DetailDrawer from './detailDrawer.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
const dictStore = useDictStore()
@@ -550,7 +529,6 @@
parkArea: ''
})
const dateRange = ref<[string, string]>()
const filterExpanded = ref(false)
const quickSearchKeyword = ref('')
const activeView = ref('mine')
@@ -687,7 +665,6 @@
searchForm.parkArea = ''
dateRange.value = undefined
quickSearchKeyword.value = ''
filterExpanded.value = false
pagination.page = 1
getList()
}
@@ -922,14 +899,3 @@
getList()
})
</script>
<style scoped lang="scss">
.quick-search-input {
width: 280px;
margin-right: 8px;
}
.quick-search-icon {
cursor: pointer;
}
</style>
+3 -1
View File
@@ -102,7 +102,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -68,6 +68,12 @@
<div class="list-toolbar">
<div class="list-toolbar-row">
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="合同编号/合同名称/审批人"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
v-if="hasContractPerm('approvalExport')"
class="btn-ghost"
@@ -77,7 +83,15 @@
<el-icon><Download /></el-icon>导出
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="getList" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="getList"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -89,7 +103,9 @@
stripe
row-key="id"
@row-dblclick="handleView"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -113,6 +129,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('contractName')"
prop="contractName"
label="合同名称"
min-width="200"
@@ -120,17 +137,18 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="审批级别" min-width="100" align="center" header-align="center">
<el-table-column label="审批级别" v-if="isColumnVisible('step')" min-width="100" align="center" header-align="center">
<template #default="{ row }">第{{ row.step }}级</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('approverName')"
prop="approverName"
label="审批人"
min-width="120"
align="center"
header-align="center"
/>
<el-table-column label="审批结果" min-width="100" align="center" header-align="center">
<el-table-column label="审批结果" v-if="isColumnVisible('approvalResult')" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag v-if="row.approvalResult === 'approved'" type="success" size="small"
>通过</el-tag
@@ -142,6 +160,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('approvalOpinion')"
prop="approvalOpinion"
label="审批意见"
min-width="200"
@@ -149,7 +168,7 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="审批时间" min-width="160" align="center" header-align="center">
<el-table-column label="审批时间" v-if="isColumnVisible('approvalTime')" min-width="160" align="center" header-align="center">
<template #default="{ row }">{{ parseTime(row.approvalTime) }}</template>
</el-table-column>
</el-table>
@@ -197,7 +216,8 @@
v-model="approvalDialog.visible"
title="审批"
width="600px"
class="dialog-form-layout order-module-dialog-mask"
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
align-center
destroy-on-close
:close-on-click-modal="false"
@@ -207,7 +227,9 @@
ref="approvalFormRef"
:model="approvalForm"
:rules="approvalRules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
>
<div class="form-grid-2">
@@ -251,6 +273,7 @@
<script setup lang="ts" name="ContractApproval">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {
@@ -279,10 +302,30 @@
import { parseTime } from '@/utils/format'
import { $modal } from '@/utils/modal'
import { useContractPerm } from '@/hooks/business/useContractPerm'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
const router = useRouter()
const { loadPerms, hasContractPerm } = useContractPerm()
const quickSearchKeyword = ref('')
const listColumnDefs = [
{ prop: 'contractName', label: '合同名称' },
{ prop: 'step', label: '审批级别' },
{ prop: 'approverName', label: '审批人' },
{ prop: 'approvalResult', label: '审批结果' },
{ prop: 'approvalOpinion', label: '审批意见' },
{ prop: 'approvalTime', label: '审批时间' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('crm_contract_approval_visible_columns', listColumnDefs)
const queryParams = ref<ApprovalQuery>({
pageNum: 1,
pageSize: 20,
@@ -318,6 +361,7 @@
const approvalList = ref<ApprovalVO[]>([])
const loading = ref(true)
const selectedRows = ref<ApprovalVO[]>([])
const submitLoading = ref(false)
const exportLoading = ref(false)
const total = ref(0)
@@ -390,16 +434,40 @@
}
const handleQuery = () => {
quickSearchKeyword.value = ''
queryParams.value.pageNum = 1
getList()
}
const resetQuery = () => {
dateRange.value = undefined
quickSearchKeyword.value = ''
queryFormRef.value?.resetFields()
handleQuery()
}
const handleQuickSearch = () => {
const keyword = quickSearchKeyword.value?.trim()
queryParams.value.contractNo = keyword || undefined
queryParams.value.contractName = keyword || undefined
queryParams.value.approverName = keyword || undefined
queryParams.value.pageNum = 1
getList()
}
const handleQuickSearchClear = () => {
quickSearchKeyword.value = ''
queryParams.value.contractNo = undefined
queryParams.value.contractName = undefined
queryParams.value.approverName = undefined
queryParams.value.pageNum = 1
getList()
}
const handleSelectionChange = (rows: ApprovalVO[]) => {
selectedRows.value = rows
}
const handleView = async (row: ApprovalVO) => {
const res = await getApproval(row.id)
viewDialog.data = res.data
@@ -442,6 +510,7 @@
}
approvalDialog.visible = false
await getList()
await refreshViewDialogIfOpen()
} catch (error) {
console.error(error)
} finally {
@@ -450,6 +519,12 @@
})
}
async function refreshViewDialogIfOpen() {
if (!viewDialog.visible || !viewDialog.data?.id) return
const res = await getApproval(viewDialog.data.id)
viewDialog.data = res.data
}
const handleExport = async () => {
if (dateRange.value && dateRange.value.length === 2) {
queryParams.value.beginTime = dateRange.value[0]
@@ -73,6 +73,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="合同编号/合同名称"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
v-if="hasContractPerm('changeAdd')"
class="btn-main"
@@ -85,8 +91,18 @@
v-if="hasContractPerm('changeRemove')"
class="btn-ghost"
type="danger"
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
:disabled="
selectedRows.length === 0 ||
!allRowsDeletableByCreator(selectedRows) ||
!selectedRows.every((row) => row.approvalStatus === 'pending')
"
:title="
selectedRows.length && !selectedRows.every((row) => row.approvalStatus === 'pending')
? '只能删除待审批状态的变更记录'
: allRowsDeletableByCreator(selectedRows)
? ''
: CREATOR_DELETE_TIP
"
@click="handleDelete()"
>
<el-icon><Delete /></el-icon>删除
@@ -124,7 +140,15 @@
<el-icon><Download /></el-icon>导出
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="getList" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="getList"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -162,6 +186,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('contractName')"
prop="contractName"
label="合同名称"
min-width="180"
@@ -169,7 +194,7 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="变更类型" min-width="120" align="center" header-align="center">
<el-table-column label="变更类型" v-if="isColumnVisible('changeType')" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-tag
:type="dictStore.getTagType('crm_contract_change_type', row.changeType)"
@@ -180,6 +205,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('changeReason')"
prop="changeReason"
label="变更原因"
min-width="200"
@@ -187,13 +213,13 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="变更金额" min-width="130" align="center" header-align="center">
<el-table-column label="变更金额" v-if="isColumnVisible('changeAmount')" min-width="130" align="center" header-align="center">
<template #default="{ row }">
<span v-if="row.changeAmount">¥{{ row.changeAmount.toLocaleString() }}</span>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column label="审批状态" min-width="100" align="center" header-align="center">
<el-table-column label="审批状态" v-if="isColumnVisible('approvalStatus')" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-tag v-if="row.approvalStatus === 'pending'" type="warning" size="small"
>待审批</el-tag
@@ -207,13 +233,14 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('approverName')"
prop="approverName"
label="审批人"
min-width="100"
align="center"
header-align="center"
/>
<el-table-column label="创建时间" min-width="160" align="center" header-align="center">
<el-table-column label="创建时间" v-if="isColumnVisible('createTime')" min-width="160" align="center" header-align="center">
<template #default="{ row }">{{ parseTime(row.createTime) }}</template>
</el-table-column>
</el-table>
@@ -235,7 +262,8 @@
v-model="dialog.visible"
:title="dialog.title"
width="800px"
class="dialog-form-layout order-module-dialog-mask"
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
align-center
destroy-on-close
:close-on-click-modal="false"
@@ -246,7 +274,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -388,6 +418,7 @@
<script setup lang="ts" name="ContractChange">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import {
@@ -431,11 +462,32 @@
allRowsDeletableByCreator,
CREATOR_DELETE_TIP
} from '@/utils/creator-delete'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
const router = useRouter()
const dictStore = useDictStore()
const { loadPerms, hasContractPerm } = useContractPerm()
const quickSearchKeyword = ref('')
const listColumnDefs = [
{ prop: 'contractName', label: '合同名称' },
{ prop: 'changeType', label: '变更类型' },
{ prop: 'changeReason', label: '变更原因' },
{ prop: 'changeAmount', label: '变更金额' },
{ prop: 'approvalStatus', label: '审批状态' },
{ prop: 'approverName', label: '审批人' },
{ prop: 'createTime', label: '创建时间' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('crm_contract_change_visible_columns', listColumnDefs)
const queryParams = ref<ChangeQuery>({
pageNum: 1,
pageSize: 20,
@@ -567,16 +619,34 @@
}
const handleQuery = () => {
quickSearchKeyword.value = ''
queryParams.value.pageNum = 1
getList()
}
const resetQuery = () => {
dateRange.value = undefined
quickSearchKeyword.value = ''
queryFormRef.value?.resetFields()
handleQuery()
}
const handleQuickSearch = () => {
const keyword = quickSearchKeyword.value?.trim()
queryParams.value.contractNo = keyword || undefined
queryParams.value.contractName = keyword || undefined
queryParams.value.pageNum = 1
getList()
}
const handleQuickSearchClear = () => {
quickSearchKeyword.value = ''
queryParams.value.contractNo = undefined
queryParams.value.contractName = undefined
queryParams.value.pageNum = 1
getList()
}
const handleSelectionChange = (selection: ChangeVO[]) => {
selectedRows.value = selection
ids.value = selection.map((item) => item.id)
@@ -594,6 +664,12 @@
viewDialog.visible = true
}
async function refreshViewDialogIfOpen() {
if (!viewDialog.visible || !viewDialog.data?.id) return
const res = await getChange(viewDialog.data.id)
viewDialog.data = res.data
}
const handleViewContract = (row: ChangeVO) => {
router.push({
path: '/crm/contract/detail',
@@ -615,6 +691,7 @@
}
dialog.visible = false
await getList()
await refreshViewDialogIfOpen()
} catch (error) {
console.error(error)
} finally {
@@ -625,20 +702,26 @@
const handleDelete = async (row?: ChangeVO) => {
const targets = row ? [row] : selectedRows.value
const deletable = filterDeletableByCreator(targets)
const pendingTargets = targets.filter((item) => item.approvalStatus === 'pending')
if (!pendingTargets.length) {
$modal.msgWarning('只能删除待审批状态的变更记录')
return
}
const deletable = filterDeletableByCreator(pendingTargets)
if (!deletable.length) {
$modal.msgWarning(CREATOR_DELETE_TIP)
return
}
if (deletable.length < targets.length) {
if (deletable.length < pendingTargets.length) {
$modal.msgWarning(CREATOR_DELETE_TIP)
return
}
const idList = row?.id ? [row.id] : ids.value
const idList = row?.id ? [row.id] : pendingTargets.map((item) => item.id)
await $modal.confirm('是否确认删除选中的变更记录?')
await delChange(idList)
$modal.msgSuccess('删除成功')
await getList()
await refreshViewDialogIfOpen()
}
async function handleDetailDelete() {
@@ -669,6 +752,7 @@
await submitChangeApproval({ id: row.id, approverIds: [] })
$modal.msgSuccess('提交成功')
await getList()
await refreshViewDialogIfOpen()
}
const handleExecute = async (row: ChangeVO) => {
@@ -676,6 +760,7 @@
await executeChange(row.id)
$modal.msgSuccess('执行成功')
await getList()
await refreshViewDialogIfOpen()
}
const handleWithdraw = async (row: ChangeVO) => {
@@ -683,6 +768,7 @@
await withdrawChange(row.id)
$modal.msgSuccess('撤回成功')
await getList()
await refreshViewDialogIfOpen()
}
const handleExport = async () => {
@@ -14,7 +14,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -38,7 +38,11 @@
<el-icon><Edit /></el-icon>修改
</el-button>
<el-button
v-if="hasContractPerm('remove') && canDeleteByCreator(detailContract)"
v-if="
hasContractPerm('remove') &&
detailContract?.status === 'draft' &&
canDeleteByCreator(detailContract)
"
class="btn-ghost"
@click="handleDetailDelete"
>
@@ -361,7 +365,14 @@
</div>
</div>
<div class="list-body list-wrap">
<el-table :data="detailPaymentList" class="list-table" stripe v-loading="detailPaymentLoading" empty-text="暂无回款记录">
<el-table
:data="detailPaymentList"
class="list-table"
stripe
v-loading="detailPaymentLoading"
empty-text="暂无回款记录"
@row-dblclick="handleDetailViewPayment"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column label="回款单号" prop="paymentNo" min-width="150" align="center" header-align="center">
<template #default="scope">
@@ -402,7 +413,14 @@
</div>
</div>
<div class="list-body list-wrap">
<el-table :data="detailInvoiceList" class="list-table" stripe v-loading="detailInvoiceLoading" empty-text="暂无开票记录">
<el-table
:data="detailInvoiceList"
class="list-table"
stripe
v-loading="detailInvoiceLoading"
empty-text="暂无开票记录"
@row-dblclick="handleDetailViewInvoice"
>
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column label="发票号码" prop="invoiceNo" min-width="150" align="center" header-align="center">
<template #default="scope">
@@ -721,11 +739,6 @@
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<el-button class="btn-ghost" @click="visible = false">
<el-icon><Close /></el-icon>关闭
</el-button>
</template>
</el-dialog>
<!-- 生成合同对话框 -->
<el-dialog
@@ -12,7 +12,7 @@
@opened="handleOpened"
>
<div class="contract-form-body">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
<!-- 合同基础信息(一行 5 列) -->
<div class="contract-basic-info">
<el-form-item label="合同编号" prop="contractNo">
@@ -128,45 +128,29 @@
<div class="parties-row">
<div class="party-panel">
<div class="party-panel__title">供方信息</div>
<el-row :gutter="12">
<el-col :span="6">
<el-form-item label="单位名称(章)" prop="partyA">
<el-input v-model="form.partyA" placeholder="请输入单位名称" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="单位地址" prop="partyAAddress">
<el-input v-model="form.partyAAddress" placeholder="请输入单位地址" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="法定代表人" prop="partyALegalPerson">
<el-input v-model="form.partyALegalPerson" placeholder="请输入法定代表人" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="代理人" prop="partyAAgent">
<el-input v-model="form.partyAAgent" placeholder="请输入代理人(可选)" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="6">
<el-form-item label="电话/传真" prop="partyAPhoneFax">
<el-input v-model="form.partyAPhoneFax" placeholder="请输入电话/传真" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开户银行" prop="partyABank">
<el-input v-model="form.partyABank" placeholder="请输入开户银行" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="账号" prop="partyAAccount">
<el-input v-model="form.partyAAccount" placeholder="请输入账号" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<div class="form-grid-3">
<el-form-item label="单位名称(章)" prop="partyA">
<el-input v-model="form.partyA" placeholder="请输入单位名称" maxlength="100" />
</el-form-item>
<el-form-item label="单位地址" prop="partyAAddress">
<el-input v-model="form.partyAAddress" placeholder="请输入单位地址" maxlength="200" />
</el-form-item>
<el-form-item label="法定代表人" prop="partyALegalPerson">
<el-input v-model="form.partyALegalPerson" placeholder="请输入法定代表人" maxlength="100" />
</el-form-item>
<el-form-item label="代理人" prop="partyAAgent">
<el-input v-model="form.partyAAgent" placeholder="请输入代理人(可选)" maxlength="100" />
</el-form-item>
<el-form-item label="电话/传真" prop="partyAPhoneFax">
<el-input v-model="form.partyAPhoneFax" placeholder="请输入电话/传真" maxlength="100" />
</el-form-item>
<el-form-item label="开户银行" prop="partyABank">
<el-input v-model="form.partyABank" placeholder="请输入开户银行" maxlength="200" />
</el-form-item>
<el-form-item label="账号" prop="partyAAccount">
<el-input v-model="form.partyAAccount" placeholder="请输入账号" maxlength="100" />
</el-form-item>
</div>
</div>
<div class="party-flow-bridge" aria-hidden="true">
@@ -192,45 +176,29 @@
<div class="party-panel">
<div class="party-panel__title">需方信息</div>
<el-row :gutter="12">
<el-col :span="6">
<el-form-item label="单位名称(章)" prop="partyB">
<el-input v-model="form.partyB" placeholder="从客户开票信息带出" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="单位地址" prop="partyBAddress">
<el-input v-model="form.partyBAddress" placeholder="从客户开票信息带出" maxlength="500" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="法定代表人" prop="partyBLegalPerson">
<el-input v-model="form.partyBLegalPerson" placeholder="从客户开票信息带出" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="代理人" prop="partyBAgent">
<el-input v-model="form.partyBAgent" placeholder="从客户开票信息带出(可选)" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="6">
<el-form-item label="电话/传真" prop="partyBPhoneFax">
<el-input v-model="form.partyBPhoneFax" placeholder="从客户开票信息带出" maxlength="100" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开户银行" prop="partyBBank">
<el-input v-model="form.partyBBank" placeholder="从客户开票信息带出" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="账号" prop="partyBAccount">
<el-input v-model="form.partyBAccount" placeholder="从客户开票信息带出" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
<div class="form-grid-3">
<el-form-item label="单位名称(章)" prop="partyB">
<el-input v-model="form.partyB" placeholder="从客户开票信息带出" maxlength="200" />
</el-form-item>
<el-form-item label="单位地址" prop="partyBAddress">
<el-input v-model="form.partyBAddress" placeholder="从客户开票信息带出" maxlength="500" />
</el-form-item>
<el-form-item label="法定代表人" prop="partyBLegalPerson">
<el-input v-model="form.partyBLegalPerson" placeholder="从客户开票信息带出" maxlength="100" />
</el-form-item>
<el-form-item label="代理人" prop="partyBAgent">
<el-input v-model="form.partyBAgent" placeholder="从客户开票信息带出(可选)" maxlength="100" />
</el-form-item>
<el-form-item label="电话/传真" prop="partyBPhoneFax">
<el-input v-model="form.partyBPhoneFax" placeholder="从客户开票信息带出" maxlength="100" />
</el-form-item>
<el-form-item label="开户银行" prop="partyBBank">
<el-input v-model="form.partyBBank" placeholder="从客户开票信息带出" maxlength="200" />
</el-form-item>
<el-form-item label="账号" prop="partyBAccount">
<el-input v-model="form.partyBAccount" placeholder="从客户开票信息带出" maxlength="100" />
</el-form-item>
</div>
</div>
</div>
</div>
@@ -2108,10 +2076,6 @@ const loadSupplierList = async () => {
border-radius: 10px;
background: #fff;
:deep(.el-row + .el-row) {
margin-top: 12px;
}
:deep(.el-form-item) {
margin-bottom: 0;
}
@@ -22,7 +22,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -21,7 +21,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -65,7 +65,6 @@
class="list-table"
stripe
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
:max-height="400"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
@@ -308,13 +307,6 @@ const handleSelectionChange = (selection: ProductVO[]) => {
selectedProducts.value = selection;
};
/** 单击行选中/取消选中产品(点击复选框列时不重复切换) */
const handleRowClick = (row: ProductVO, _column: unknown, event: MouseEvent) => {
const target = event.target as HTMLElement
if (target.closest('.el-checkbox')) return
tableRef.value?.toggleRowSelection(row)
}
/** 移除已选产品 */
const handleRemoveSelection = (product: ProductVO) => {
const index = selectedProducts.value.findIndex((p) => p.id === product.id);
@@ -2,7 +2,7 @@
<el-dialog v-model="visible" title="合同续签" width="800px" :close-on-click-modal="false" @close="handleClose">
<el-alert title="提示" type="info" :closable="false" class="mb-4"> 系统将自动复制原合同信息,您可以修改金额、期限等字段 </el-alert>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="原合同" prop="parentContractId">
+6 -2
View File
@@ -29,7 +29,7 @@
<el-icon><Edit /></el-icon>修改
</el-button>
<el-button
v-if="hasContractPerm('remove') && canDeleteByCreator(contract)"
v-if="hasContractPerm('remove') && contract?.status === 'draft' && canDeleteByCreator(contract)"
type="danger"
@click="handleDelete"
>
@@ -75,7 +75,7 @@
<p v-if="contract" class="detail-subtitle">{{ contract.contractNo }} — {{ contract.contractName }}</p>
<el-tabs v-model="activeTab" class="panel-pill-tabs panel-pill-tabs--lg">
<el-tabs v-model="activeTab" class="panel-tabs">
<!-- 合同信息 -->
<el-tab-pane label="合同信息" name="contract">
<el-form label-width="120px" v-if="contract">
@@ -1291,6 +1291,10 @@ const handleEdit = () => {
/** 删除合同 */
const handleDelete = async () => {
if (contract.value?.status !== 'draft') {
$modal.msgWarning('只能删除草稿状态的合同');
return;
}
try {
await $modal.confirm('是否确认删除该合同?删除后将无法恢复。');
await delContract([contract.value?.id]);
+32 -53
View File
@@ -7,7 +7,7 @@
<el-tabs
v-model="activeView"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleViewChange"
>
<el-tab-pane label="我的合同" name="my" />
@@ -42,57 +42,39 @@
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="queryParams.customerName" placeholder="客户名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</div>
<div v-show="filterExpanded" id="contract-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="合同类型" prop="contractType">
<el-select v-model="queryParams.contractType" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_contract_type')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="合同状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_contract_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="审批状态" prop="approvalStatus">
<el-select v-model="queryParams.approvalStatus" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_approval_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
</div>
<el-form-item label="合同类型" prop="contractType">
<el-select v-model="queryParams.contractType" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_contract_type')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="合同状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_contract_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="审批状态" prop="approvalStatus">
<el-select v-model="queryParams.approvalStatus" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_approval_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="contract-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="resetQuery">
<el-icon><RefreshLeft /></el-icon>重置
@@ -523,8 +505,6 @@ import {
type ContractVO
} from '@/api/crm/contract';
import {
ArrowDownBold,
ArrowUpBold,
CircleCheck,
CircleClose,
Clock,
@@ -665,7 +645,6 @@ const contractList = ref<ContractVO[]>([]);
const loading = ref(true);
const total = ref(0);
const expandLoadingMap = reactive<Record<string, boolean>>({});
const filterExpanded = ref(false);
const selectedRows = ref<ContractVO[]>([]);
const single = ref(true);
const multiple = ref(true);
@@ -53,6 +53,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="模板名称/模板编号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
v-if="hasContractPerm('templateAdd')"
class="btn-main"
@@ -88,7 +94,15 @@
<el-icon><Download /></el-icon>导出
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="getList" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="getList"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -123,7 +137,7 @@
<el-link type="primary" @click="handleView(row)">{{ row.templateName }}</el-link>
</template>
</el-table-column>
<el-table-column label="模板类型" min-width="120" align="center" header-align="center">
<el-table-column label="模板类型" v-if="isColumnVisible('templateType')" min-width="120" align="center" header-align="center">
<template #default="{ row }">
<el-tag
:type="dictStore.getTagType('crm_contract_type', row.templateType)"
@@ -134,6 +148,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('templateNo')"
prop="templateNo"
label="模板编号"
min-width="150"
@@ -141,7 +156,7 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column label="状态" min-width="100" align="center" header-align="center">
<el-table-column label="状态" v-if="isColumnVisible('isActive')" min-width="100" align="center" header-align="center">
<template #default="{ row }">
<el-switch
v-if="hasContractPerm('templateEdit')"
@@ -156,6 +171,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('usageCount')"
prop="usageCount"
label="使用次数"
min-width="100"
@@ -163,13 +179,14 @@
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('sort')"
prop="sort"
label="排序"
min-width="80"
align="center"
header-align="center"
/>
<el-table-column label="创建时间" min-width="160" align="center" header-align="center">
<el-table-column label="创建时间" v-if="isColumnVisible('createTime')" min-width="160" align="center" header-align="center">
<template #default="{ row }">
{{ parseTime(row.createTime) }}
</template>
@@ -193,7 +210,8 @@
v-model="dialog.visible"
:title="dialog.title"
width="900px"
class="dialog-form-layout order-module-dialog-mask"
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
align-center
destroy-on-close
:close-on-click-modal="false"
@@ -204,7 +222,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -326,6 +346,7 @@
<script setup lang="ts" name="ContractTemplate">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { ref, reactive, computed, onMounted } from 'vue'
import {
Document,
@@ -366,10 +387,30 @@
allRowsDeletableByCreator,
CREATOR_DELETE_TIP
} from '@/utils/creator-delete'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
const dictStore = useDictStore()
const { loadPerms, hasContractPerm } = useContractPerm()
const quickSearchKeyword = ref('')
const listColumnDefs = [
{ prop: 'templateType', label: '模板类型' },
{ prop: 'templateNo', label: '模板编号' },
{ prop: 'isActive', label: '状态' },
{ prop: 'usageCount', label: '使用次数' },
{ prop: 'sort', label: '排序', defaultVisible: false },
{ prop: 'createTime', label: '创建时间' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('crm_contract_template_visible_columns', listColumnDefs)
const queryParams = ref<TemplateQuery>({
pageNum: 1,
pageSize: 20,
@@ -481,15 +522,33 @@
}
const handleQuery = () => {
quickSearchKeyword.value = ''
queryParams.value.pageNum = 1
getList()
}
const resetQuery = () => {
quickSearchKeyword.value = ''
queryFormRef.value?.resetFields()
handleQuery()
}
const handleQuickSearch = () => {
const keyword = quickSearchKeyword.value?.trim()
queryParams.value.templateName = keyword || undefined
queryParams.value.templateNo = keyword || undefined
queryParams.value.pageNum = 1
getList()
}
const handleQuickSearchClear = () => {
quickSearchKeyword.value = ''
queryParams.value.templateName = undefined
queryParams.value.templateNo = undefined
queryParams.value.pageNum = 1
getList()
}
const handleSelectionChange = (selection: TemplateVO[]) => {
selectedRows.value = selection
ids.value = selection.map((item) => item.id)
@@ -575,7 +575,7 @@ function viewTableSection(title: string, fields: ViewField[]): ViewSection {
title,
fields,
fieldLayout: 'table',
tableMode: 'pair'
tableMode: 'triple'
}
}
@@ -604,12 +604,13 @@ const viewSections = computed<ViewSection[]>(() => {
tagField('成交状态', 'crm_deal_status', c.dealStatus),
{ label: '负责人', value: c.ownerUserName },
{ label: '创建人', value: c.createByName || c.createBy },
{ label: '创建时间', value: c.createTime },
{ label: '创建时间', value: c.createTime, long: false },
{ label: '跟进天数', value: `${calculateDays(c.createTime)} 天` },
{
label: '下次联系时间',
value: c.nextTime,
tagType: isOverdue.value ? 'danger' : isToday.value ? 'warning' : undefined
tagType: isOverdue.value ? 'danger' : isToday.value ? 'warning' : undefined,
long: false
}
]
@@ -1017,9 +1018,9 @@ defineExpose({ open })
}
.view-split-left {
flex: 0 0 580px;
width: 580px;
max-width: 50%;
flex: 0 0 720px;
width: 720px;
max-width: 55%;
}
.view-split-left .view-hero {
@@ -1053,5 +1054,28 @@ defineExpose({ open })
.view-split-right .customer-view-tabs-section {
padding: 0;
}
/* 左侧信息区:三列表格模式(triple = 一行三组标签/值) */
.view-split-left .view-kv-table-wrap {
border-color: #eef4f1;
border-radius: 8px;
}
.view-split-left .view-kv-table th {
background: #f8fafc;
font-size: 12px;
padding: 8px 10px;
}
.view-split-left .view-kv-table td {
font-size: 13px;
padding: 8px 12px;
}
.view-split-left .view-kv-table tr.row-long td {
font-weight: 400;
color: #475569;
line-height: 1.55;
}
}
</style>
@@ -13,7 +13,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -12,7 +12,7 @@
<div>客户【{{ customerName }}】即将标记为已成交状态</div>
</el-alert>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="100px" class="crm-dialog-form">
<el-form-item prop="dealAmount">
<template #label>成交金额<span class="form-label-tip">单位:元</span></template>
<el-input-number
@@ -13,7 +13,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -12,7 +12,7 @@
<div>已选择 {{ selectedIds.length }} 位客户,将统一放入公海池</div>
</el-alert>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="100px" class="crm-dialog-form">
<el-form-item label="放入原因" prop="reason">
<el-input
v-model="form.reason"
@@ -12,7 +12,7 @@
<div>已选择 {{ selectedIds.length }} 位客户,将统一转移给指定的负责人</div>
</el-alert>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="100px" class="crm-dialog-form">
<el-form-item label="新负责人" prop="newOwnerUserId">
<UserSelector v-model="form.newOwnerUserId" placeholder="请选择新负责人" @change="handleOwnerChange" />
</el-form-item>
@@ -14,7 +14,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -114,8 +114,8 @@
</el-table>
</div>
<el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body @close="handleClose">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="dialog-form-layout">
<el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" class="dialog-form-layout" align-center append-to-body @close="handleClose">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="100px" class="crm-dialog-form">
<div class="form-grid-2">
<el-form-item label="单位名称(章)" prop="companyName" class="form-col-span-2">
<el-input v-model="form.companyName" placeholder="请输入单位名称" maxlength="200" />
+82 -87
View File
@@ -8,7 +8,7 @@
<el-tabs
v-model="activeView"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleViewChange"
>
<el-tab-pane label="我的客户" name="mine" />
@@ -57,90 +57,65 @@
/>
</el-select>
</el-form-item>
</div>
<div
v-show="filterExpanded"
id="customer-filter-extra"
class="filter-extra"
>
<div class="filter-grid">
<el-form-item label="客户来源">
<el-select v-model="searchForm.source" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_clue_source')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="客户状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_customer_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="成交状态">
<el-select v-model="searchForm.dealStatus" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_deal_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="所在园区">
<el-select
v-model="searchForm.parkArea"
placeholder="全部"
clearable
filterable
>
<el-option
v-for="opt in dictStore.getOptions('crm_customer_park')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</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 label="客户来源">
<el-select v-model="searchForm.source" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_clue_source')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-form-item>
</div>
</el-select>
</el-form-item>
<el-form-item label="客户状态">
<el-select v-model="searchForm.status" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_customer_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="成交状态">
<el-select v-model="searchForm.dealStatus" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_deal_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
<el-form-item label="所在园区">
<el-select
v-model="searchForm.parkArea"
placeholder="全部"
clearable
filterable
>
<el-option
v-for="opt in dictStore.getOptions('crm_customer_park')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</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-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="customer-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
<span class="filter-hint filter-hint--inline"
>按客户名称、编号模糊查询;默认展示一行常用条件。</span
>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -160,6 +135,13 @@
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="客户名称/编号/手机号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增客户
@@ -672,8 +654,6 @@ import {
Search,
RefreshLeft,
DocumentCopy,
ArrowUpBold,
ArrowDownBold,
Edit,
} from '@element-plus/icons-vue'
import { useDictStore } from '@/store/modules/dict'
@@ -704,6 +684,7 @@ import IntoPoolForm from './IntoPoolDialog.vue'
import FollowForm from './FollowFormDialog.vue'
import DealForm from './DealDialog.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
const dictStore = useDictStore()
@@ -721,8 +702,7 @@ const searchForm = reactive({
})
const dateRange = ref<[string, string]>()
/** 筛选区展开更多条件(默认仅展示首行四列) */
const filterExpanded = ref(false)
const quickSearchKeyword = ref('')
// 视图类型
const activeView = ref('mine')
@@ -828,6 +808,8 @@ function buildQueryParams(): Record<string, any> {
params.beginTime = dateRange.value[0]
params.endTime = dateRange.value[1]
}
const keyword = quickSearchKeyword.value?.trim()
if (keyword) params.keyword = keyword
return params
}
@@ -856,6 +838,7 @@ async function getList() {
/** 搜索 */
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
@@ -871,7 +854,18 @@ function handleReset() {
searchForm.dealStatus = ''
searchForm.parkArea = ''
dateRange.value = undefined
filterExpanded.value = false
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
function handleQuickSearch() {
pagination.page = 1
getList()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
@@ -1016,6 +1010,7 @@ async function handleExport() {
watchDebounced(
() => ({ ...searchForm, dateRange: dateRange.value }),
() => {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
},
@@ -52,6 +52,13 @@
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="规则名称"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" v-auth="'add'" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增规则
@@ -96,7 +103,15 @@
手动执行
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="getList" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="getList"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -109,6 +124,7 @@
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleEdit"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
@@ -129,6 +145,7 @@
/>
<el-table-column
prop="ruleType"
v-if="isColumnVisible('ruleType')"
label="规则类型"
min-width="140"
align="center"
@@ -141,6 +158,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('timeoutDays')"
prop="timeoutDays"
label="超时天数"
min-width="100"
@@ -152,6 +170,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('protectDays')"
prop="protectDays"
label="保护天数"
min-width="100"
@@ -163,6 +182,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('status')"
prop="status"
label="状态"
min-width="80"
@@ -176,6 +196,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('sort')"
prop="sort"
label="排序"
min-width="80"
@@ -183,6 +204,7 @@
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('createTime')"
prop="createTime"
label="创建时间"
min-width="160"
@@ -212,6 +234,8 @@
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import {
Plus,
Close,
@@ -239,6 +263,24 @@ const searchForm = reactive({
ruleType: '',
status: '',
})
const quickSearchKeyword = ref('')
const listColumnDefs = [
{ prop: 'ruleType', label: '规则类型' },
{ prop: 'timeoutDays', label: '超时天数' },
{ prop: 'protectDays', label: '保护天数' },
{ prop: 'status', label: '状态' },
{ prop: 'sort', label: '排序', defaultVisible: false },
{ prop: 'createTime', label: '创建时间' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('crm_pool_rule_visible_columns', listColumnDefs)
// 数据列表
const ruleList = ref<any[]>([])
@@ -267,10 +309,11 @@ const ruleTypeTagType = (type: string) => {
const getList = async () => {
loading.value = true
try {
const quickKeyword = quickSearchKeyword.value?.trim()
const res = await fetchPoolRuleList({
page: pagination.page,
pageSize: pagination.pageSize,
poolName: searchForm.poolName || undefined,
poolName: quickKeyword || searchForm.poolName || undefined,
ruleType: searchForm.ruleType || undefined,
status: searchForm.status || undefined,
})
@@ -284,12 +327,25 @@ const getList = async () => {
}
const handleSearch = () => {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
const handleReset = () => {
Object.assign(searchForm, { poolName: '', ruleType: '', status: '' })
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
const handleQuickSearch = () => {
pagination.page = 1
getList()
}
const handleQuickSearchClear = () => {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
@@ -13,7 +13,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -13,7 +13,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -13,7 +13,9 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
+27 -75
View File
@@ -6,26 +6,6 @@
<h2 class="title-bar-heading">公海池</h2>
</div>
<!-- 统计信息 -->
<div class="pool-statistics">
<div class="stat-item">
<div class="stat-value">{{ poolStatistics.totalCount }}</div>
<div class="stat-label">公海池客户总数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ poolStatistics.todayCount }}</div>
<div class="stat-label">今日新增</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ poolStatistics.weekCount }}</div>
<div class="stat-label">本周新增</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ poolStatistics.keyCount }}</div>
<div class="stat-label">重点客户</div>
</div>
</div>
<!-- 筛选区 -->
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
@@ -78,6 +58,13 @@
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="客户名称/编号/手机号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" :disabled="multiple" @click="handleClaim()">
<el-icon><Select /></el-icon>
领取客户
@@ -553,6 +540,7 @@ import {
import { useDictStore } from '@/store/modules/dict'
import { fetchPoolCustomerList } from '@/api/crm/pool'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import ClaimDialog from './components/ClaimDialog.vue'
import AssignDialog from './components/AssignDialog.vue'
@@ -566,6 +554,7 @@ const searchForm = reactive({
customerNo: '',
level: '',
})
const quickSearchKeyword = ref('')
// 数据列表
const customerList = ref<any[]>([])
@@ -578,14 +567,6 @@ const single = ref(true)
const multiple = ref(true)
const ids = ref<string[]>([])
// 公海池统计
const poolStatistics = reactive({
totalCount: 0,
todayCount: 0,
weekCount: 0,
keyCount: 0,
})
// 列配置
const listColumnDefs = [
{ prop: 'shortName', label: '客户简称' },
@@ -651,30 +632,18 @@ const customerDetailRef = ref()
const getList = async () => {
loading.value = true
try {
const quickKeyword = quickSearchKeyword.value?.trim()
const filterKeyword =
searchForm.customerName?.trim() || searchForm.customerNo?.trim() || undefined
const res = await fetchPoolCustomerList({
page: pagination.page,
pageSize: pagination.pageSize,
customerName: searchForm.customerName || undefined,
customerNo: searchForm.customerNo || undefined,
keyword: quickKeyword || filterKeyword,
level: searchForm.level || undefined,
})
const data = (res as any)?.data || res || {}
customerList.value = data.list || data.rows || []
pagination.total = data.total ?? 0
const today = new Date().toISOString().split('T')[0]
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
const weekAgoStr = weekAgo.toISOString().split('T')[0]
poolStatistics.totalCount = pagination.total
poolStatistics.todayCount = customerList.value.filter(
(item: any) => item.intoPoolTime && item.intoPoolTime.startsWith(today),
).length
poolStatistics.weekCount = customerList.value.filter(
(item: any) => item.intoPoolTime && item.intoPoolTime >= weekAgoStr,
).length
poolStatistics.keyCount = customerList.value.filter((item: any) => item.level === 'key').length
} catch (error) {
console.error(error)
} finally {
@@ -683,12 +652,25 @@ const getList = async () => {
}
const handleSearch = () => {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
const handleReset = () => {
Object.assign(searchForm, { customerName: '', customerNo: '', level: '' })
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
const handleQuickSearch = () => {
pagination.page = 1
getList()
}
const handleQuickSearchClear = () => {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
@@ -731,6 +713,7 @@ const handleMoreCommand = (cmd: string, row: any) => {
watchDebounced(
searchForm,
() => {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
},
@@ -741,34 +724,3 @@ onMounted(() => {
getList()
})
</script>
<style scoped lang="scss">
.pool-statistics {
display: flex;
gap: 24px;
padding: 16px 24px;
margin-bottom: 16px;
background: var(--el-bg-color);
border-radius: 8px;
border: 1px solid var(--el-border-color-lighter);
}
.stat-item {
flex: 1;
text-align: center;
padding: 8px 0;
.stat-value {
font-size: 28px;
font-weight: 700;
color: var(--el-color-primary);
line-height: 1.2;
}
.stat-label {
font-size: 13px;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
}
</style>
@@ -1,6 +1,15 @@
<template>
<el-dialog v-model="visible" title="批量修改预计成交日期" width="600px" :close-on-click-modal="false">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-dialog
v-model="visible"
title="批量修改预计成交日期"
width="600px"
class="dialog-form-layout"
align-center
destroy-on-close
append-to-body
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
<!-- 已选择商机信息 -->
<el-alert :title="`已选择 ${selectedIds.length} 个商机`" type="info" :closable="false" show-icon style="margin-bottom: 20px" />
@@ -1,6 +1,15 @@
<template>
<el-dialog v-model="visible" title="批量修改商机阶段" width="700px" :close-on-click-modal="false">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-dialog
v-model="visible"
title="批量修改商机阶段"
width="700px"
class="dialog-form-layout"
align-center
destroy-on-close
append-to-body
:close-on-click-modal="false"
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
<!-- 已选择商机信息 -->
<el-alert :title="`已选择 ${selectedIds.length} 个商机`" type="info" :closable="false" show-icon style="margin-bottom: 20px" />
@@ -22,7 +22,7 @@
<el-descriptions-item label="当前赢单率" :span="2">{{ opportunity?.winRate }}%</el-descriptions-item>
</el-descriptions>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
<el-form-item label="新阶段" prop="stage">
<el-select v-model="form.stage" placeholder="请选择新阶段" style="width: 100%" @change="handleStageChange">
<el-option
@@ -13,7 +13,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -19,7 +19,7 @@
<el-descriptions-item label="跟进次数" :span="2">{{ opportunity?.followCount || 0 }}次</el-descriptions-item>
</el-descriptions>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
<el-form-item label="输单原因" prop="lostReason">
<el-select v-model="form.lostReason" placeholder="请选择输单原因" style="width: 100%" @change="handleReasonChange">
<el-option label="价格因素" value="price" />
@@ -19,7 +19,7 @@
<el-descriptions-item label="预估赢单率" :span="2">{{ opportunity?.winRate }}%</el-descriptions-item>
</el-descriptions>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
<el-form-item label="实际成交日期" prop="actualDealDate">
<el-date-picker
v-model="form.actualDealDate"
@@ -33,7 +33,7 @@
v-if="hasOpportunityPerm('remove')"
class="btn-ghost"
type="danger"
:disabled="opportunityInfo.status === 'won' || !canDeleteByCreator(opportunityInfo)"
:disabled="opportunityInfo.status === 'won' || opportunityInfo.status === 'lost' || !canDeleteByCreator(opportunityInfo)"
:title="canDeleteByCreator(opportunityInfo) ? '' : CREATOR_DELETE_TIP"
@click="handleDelete"
>
@@ -183,10 +183,8 @@ import {
Promotion,
Right
} from '@element-plus/icons-vue'
import SystemViewDialog, {
type ViewField,
type ViewSection
} from '@/components/business/system-view-dialog/index.vue'
import SystemViewDialog, { type ViewField, type ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { crmField, crmLongField } from '@/utils/crm/view-fields'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import { useDictStore } from '@/store/modules/dict'
import { useUserStore } from '@/store/modules/user'
@@ -286,15 +284,6 @@ function tagField(
}
}
function viewTableSection(title: string, fields: ViewField[]): ViewSection {
return {
title,
fields,
fieldLayout: 'table',
tableMode: 'dual'
}
}
const viewHeroMeta = computed(() => {
void dictStore.loaded
const o = opportunityInfo.value
@@ -312,17 +301,17 @@ const viewSections = computed<ViewSection[]>(() => {
if (!o?.id) return []
const basicFields: ViewField[] = [
{ label: '商机编号', value: o.opportunityNo },
{ label: '客户名称', value: o.customerName },
{ label: '联系人', value: o.contactName },
crmField('商机编号', o.opportunityNo),
crmField('客户名称', o.customerName),
crmField('联系人', o.contactName),
tagField('商机阶段', 'crm_opportunity_stage', o.stage),
{ label: '阶段进度', value: o.stagePercent != null ? `${o.stagePercent}%` : '—' },
{ label: '预估赢单率', value: o.winRate != null ? `${o.winRate}%` : '—' },
crmField('阶段进度', o.stagePercent != null ? `${o.stagePercent}%` : '—'),
crmField('预估赢单率', o.winRate != null ? `${o.winRate}%` : '—'),
tagField('商机状态', 'crm_opportunity_status', o.status),
{ label: '商机金额(元)', value: formatMoney(o.amount) },
{ label: '预计成交日期', value: o.expectDealDate },
{ label: '实际成交日期', value: o.actualDealDate },
{ label: '负责人', value: o.ownerUserName },
crmField('商机金额(元)', formatMoney(o.amount)),
crmField('预计成交日期', o.expectDealDate),
crmField('实际成交日期', o.actualDealDate),
crmField('负责人', o.ownerUserName),
{
label: '商机类型',
value: resolveOpportunityTypeLabel(dictStore, o.type),
@@ -336,40 +325,50 @@ const viewSections = computed<ViewSection[]>(() => {
]
const trackFields: ViewField[] = [
{ label: '跟进次数', value: `${o.followCount ?? 0} 次` },
{ label: '产品数量', value: `${o.productCount ?? 0} 个` },
{ label: '最后跟进时间', value: formatDateTime(o.lastTime) },
{ label: '下次联系时间', value: formatDateTime(o.nextTime) },
{ label: '阶段变更时间', value: formatDateTime(o.stageChangeTime) },
{ label: '创建人', value: o.createByName },
{ label: '创建时间', value: formatDateTime(o.createTime) },
{ label: '更新时间', value: formatDateTime(o.updateTime) }
crmField('跟进次数', `${o.followCount ?? 0} 次`),
crmField('产品数量', `${o.productCount ?? 0} 个`),
crmField('最后跟进时间', formatDateTime(o.lastTime), { long: false }),
crmField('下次联系时间', formatDateTime(o.nextTime), { long: false }),
crmField('阶段变更时间', formatDateTime(o.stageChangeTime), { long: false }),
crmField('创建人', o.createByName),
crmField('创建时间', formatDateTime(o.createTime), { long: false }),
crmField('更新时间', formatDateTime(o.updateTime), { long: false })
]
const sections: ViewSection[] = [
viewTableSection('基本信息', basicFields),
viewTableSection('跟进统计', trackFields)
{ title: '基本信息', fields: basicFields, fieldLayout: 'table', tableMode: 'triple' },
{ title: '跟进统计', fields: trackFields, fieldLayout: 'table', tableMode: 'triple' }
]
if (o.competitors || o.keyFactors || o.description) {
sections.push(
viewTableSection('业务信息', [
{ label: '竞争对手', value: o.competitors, span: true },
{ label: '关键决策因素', value: o.keyFactors, span: true },
{ label: '商机描述', value: o.description, span: true }
])
)
sections.push({
title: '业务信息',
fieldLayout: 'table',
tableMode: 'triple',
fields: [
crmField('竞争对手', o.competitors, { long: false, fullSpan: false }),
crmField('关键决策因素', o.keyFactors, { long: false, fullSpan: false }),
crmLongField('商机描述', o.description, { fullSpan: true })
]
})
}
if (o.status === 'lost' && o.lostReason) {
sections.push({
title: '输单信息',
fields: [{ label: '输单原因', value: o.lostReason, span: true }]
fieldLayout: 'table',
tableMode: 'triple',
fields: [crmLongField('输单原因', o.lostReason, { fullSpan: true })]
})
}
if (o.remark) {
sections.push(viewTableSection('备注', [{ label: '备注', value: o.remark, span: true }]))
sections.push({
title: '备注',
fieldLayout: 'table',
tableMode: 'triple',
fields: [crmLongField('备注', o.remark, { fullSpan: true })]
})
}
return sections
@@ -473,6 +472,10 @@ const handleEditFollow = (item: OpportunityFollowVO) => {
const handleDelete = async () => {
if (!opportunityInfo.value?.id) return
if (opportunityInfo.value.status === 'won' || opportunityInfo.value.status === 'lost') {
ElMessage.warning('已赢单或已输单的商机不允许删除')
return
}
await ElMessageBox.confirm('是否确认删除该商机?删除后将无法恢复。', '提示', { type: 'warning' })
await delOpportunity([opportunityInfo.value.id])
ElMessage.success('删除成功')
@@ -620,9 +623,9 @@ defineExpose({ open })
}
.view-split-left {
flex: 0 0 580px;
width: 580px;
max-width: 50%;
flex: 0 0 800px;
width: 800px;
max-width: 55%;
}
.view-split-right {
@@ -661,5 +664,28 @@ defineExpose({ open })
.view-split-right .opportunity-view-tabs-section {
padding: 0;
}
/* 左侧信息区:三列表格模式(triple = 一行三组标签/值) */
.view-split-left .view-kv-table-wrap {
border-color: #eef4f1;
border-radius: 8px;
}
.view-split-left .view-kv-table th {
background: #f8fafc;
font-size: 12px;
padding: 8px 10px;
}
.view-split-left .view-kv-table td {
font-size: 13px;
padding: 8px 12px;
}
.view-split-left .view-kv-table tr.row-long td {
font-weight: 400;
color: #475569;
line-height: 1.55;
}
}
</style>
@@ -14,11 +14,13 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<div class="form-grid-4">
<div class="form-grid-3">
<el-form-item label="商机名称" prop="opportunityName">
<el-input v-model="form.opportunityName" placeholder="请输入商机名称" maxlength="200" />
</el-form-item>
+30 -39
View File
@@ -8,7 +8,7 @@
<el-tabs
v-model="activeView"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleViewChange"
>
<el-tab-pane label="我的商机" name="mine" />
@@ -52,38 +52,20 @@
@keyup.enter="handleQuery"
/>
</el-form-item>
</div>
<div v-show="filterExpanded" id="opportunity-filter-extra" class="filter-extra">
<div class="filter-grid">
<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>
<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-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="opportunity-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="resetQuery">
<el-icon><RefreshLeft /></el-icon>重置
@@ -137,8 +119,20 @@
v-if="hasOpportunityPerm('remove')"
class="btn-ghost"
type="danger"
:disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
:title="canDeleteByCreator(selectedRows[0]) ? '' : CREATOR_DELETE_TIP"
:disabled="
selectedRows.length !== 1 ||
!canDeleteByCreator(selectedRows[0]) ||
selectedRows[0]?.status === 'won' ||
selectedRows[0]?.status === 'lost'
"
:title="
selectedRows.length === 1 &&
(selectedRows[0]?.status === 'won' || selectedRows[0]?.status === 'lost')
? '已赢单或已输单的商机不允许删除'
: canDeleteByCreator(selectedRows[0])
? ''
: CREATOR_DELETE_TIP
"
@click="handleDelete()"
>
<el-icon><Delete /></el-icon>删除
@@ -507,7 +501,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form ref="assignFormRef" :model="assignForm" :rules="assignRules" label-position="top">
<el-form ref="assignFormRef" :model="assignForm" :rules="assignRules" label-position="right" label-width="100px" class="crm-dialog-form">
<el-form-item label="负责人" prop="ownerUserId">
<el-select
v-model="assignForm.ownerUserId"
@@ -571,8 +565,6 @@
RefreshLeft,
Plus,
User,
ArrowUpBold,
ArrowDownBold,
Edit,
Delete
} from '@element-plus/icons-vue'
@@ -607,7 +599,6 @@
const router = useRouter()
const { loadPerms, hasOpportunityPerm } = useOpportunityPerm()
const filterExpanded = ref(false)
const selectedRows = ref<OpportunityVO[]>([])
const exportLoading = ref(false)
const assignLoading = ref(false)
@@ -831,8 +822,8 @@
const id = row?.id || ids.value[0]
const opportunity =
row || opportunityList.value[opportunityList.value.findIndex((item) => item.id === id)]
if (opportunity && opportunity.status === 'won') {
ElMessage.warning('已赢单的商机不允许删除')
if (opportunity && (opportunity.status === 'won' || opportunity.status === 'lost')) {
ElMessage.warning('已赢单或已输单的商机不允许删除')
return
}
const targets = row
@@ -50,6 +50,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="分类名称"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd()">
<el-icon><Plus /></el-icon>
新增
@@ -83,7 +89,15 @@
删除
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="getList" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="getList"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -100,8 +114,10 @@
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
highlight-current-row
@current-change="handleCurrentChange"
@selection-change="handleSelectionChange"
@row-dblclick="handleRowEdit"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column
prop="categoryName"
@@ -112,6 +128,7 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('orderNum')"
prop="orderNum"
label="排序"
min-width="80"
@@ -119,6 +136,7 @@
header-align="center"
/>
<el-table-column
v-if="isColumnVisible('status')"
prop="status"
label="状态"
min-width="80"
@@ -135,6 +153,7 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('createTime')"
prop="createTime"
label="创建时间"
min-width="160"
@@ -149,7 +168,7 @@
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="560px"
width="640px"
class="dialog-form-layout"
align-center
destroy-on-close
@@ -160,7 +179,9 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -191,7 +212,7 @@
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-select v-model="formData.status" placeholder="请选择状态" style="width: 100%">
<el-option
v-for="opt in dictStore.getOptions('sys_normal_disable')"
:key="opt.value"
@@ -224,6 +245,8 @@
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { ref, reactive, onMounted, nextTick, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance } from 'element-plus'
@@ -247,6 +270,20 @@ import {
const dictStore = useDictStore()
const listColumnDefs = [
{ prop: 'orderNum', label: '排序' },
{ prop: 'status', label: '状态' },
{ prop: 'createTime', label: '创建时间' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('crm_product_category_visible_columns', listColumnDefs)
// ── 列表数据 ──
const categoryList = ref<any[]>([])
const loading = ref(false)
@@ -284,13 +321,19 @@ const searchForm = reactive({
categoryName: '',
status: '',
})
const quickSearchKeyword = ref('')
// ── 获取列表(树形) ──
async function getList() {
loading.value = true
try {
const params: Record<string, any> = {}
if (searchForm.categoryName) params.categoryName = searchForm.categoryName
const quickKeyword = quickSearchKeyword.value?.trim()
if (quickKeyword) {
params.categoryName = quickKeyword
} else if (searchForm.categoryName) {
params.categoryName = searchForm.categoryName
}
if (searchForm.status) params.status = searchForm.status
const res = await fetchCategoryTree(params)
categoryList.value = (res as any)?.data || res || []
@@ -316,17 +359,29 @@ async function getCategoryTreeSelect() {
// ── 筛选操作 ──
function handleSearch() {
quickSearchKeyword.value = ''
getList()
}
function handleReset() {
searchForm.categoryName = ''
searchForm.status = ''
quickSearchKeyword.value = ''
getList()
}
function handleQuickSearch() {
getList()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
getList()
}
// ── 表格行选中 ──
function handleCurrentChange(row: any | null) {
selectedId.value = row?.id
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows
selectedId.value = rows.length === 1 ? rows[0]?.id : undefined
}
// ── 展开/折叠 ──
@@ -361,18 +416,27 @@ function handleAddChild() {
// ── 修改 ──
async function handleUpdate() {
if (!selectedId.value) {
await openEditForRow()
}
async function handleRowEdit(row: any) {
await openEditForRow(row)
}
async function openEditForRow(row?: any) {
const targetId = row?.id ?? selectedId.value
if (!targetId) {
ElMessage.warning('请选择一条数据进行修改')
return
}
resetForm()
await getCategoryTreeSelect()
// 从树中找到选中节点,填充表单
const node = findNodeById(categoryList.value, selectedId.value)
const node = row ?? findNodeById(categoryList.value, targetId)
if (!node) {
ElMessage.warning('未找到选中的数据')
return
}
selectedId.value = node.id
formData.id = node.id
formData.parentId = node.parentId || ''
formData.categoryName = node.categoryName
+164 -38
View File
@@ -60,6 +60,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="产品名称/编号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增产品
@@ -279,7 +285,9 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -337,7 +345,7 @@
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="formData.status" placeholder="请选择状态">
<el-select v-model="formData.status" placeholder="请选择状态" style="width: 100%">
<el-option
v-for="opt in dictStore.getOptions('crm_product_status')"
:key="opt.value"
@@ -393,11 +401,14 @@
<!-- 详情弹窗 -->
<SystemViewDialog
v-model:visible="detailDialogVisible"
dialog-class="crm-product-view-dialog"
:title="detailData?.productName || '产品详情'"
:icon="Box"
:code="detailData?.productNo"
code-label="产品编号"
:sections="productViewSections"
dense
width="1040px"
modal-class="order-module-dialog-mask"
append-to-body
>
@@ -423,8 +434,8 @@
v-for="(img, index) in productImageList"
:key="`${img}-${index}`"
:src="img"
:width="80"
:height="80"
:width="48"
:height="48"
/>
</div>
<p v-else class="view-empty-hint">暂无图片</p>
@@ -441,7 +452,7 @@
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top">
<el-form label-position="right" label-width="100px" class="crm-dialog-form">
<el-form-item label="导入模板">
<el-button type="primary" @click="downloadTemplate">
<el-icon><Download /></el-icon>下载模板
@@ -511,6 +522,7 @@ import {
} from '@/utils/creator-delete'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import {
fetchCategoryTree,
@@ -578,6 +590,7 @@ const searchForm = reactive({
categoryId: '',
status: '',
})
const quickSearchKeyword = ref('')
const formDialogVisible = ref(false)
const isEdit = ref(false)
@@ -634,23 +647,7 @@ const productViewSections = computed<ViewSection[]>(() => {
const d = detailData.value
if (!d) return []
const basicFields: ViewField[] = [
crmField('产品名称', d.productName),
crmField('产品分类', d.categoryName),
crmField('规格型号', d.model),
crmField('计量单位', formatProductUnit(d.unit)),
crmField('条码', d.barcode),
crmDictField('状态', 'crm_product_status', d.status, dictStore),
crmField('库存数量', d.stock ?? 0),
crmField('库存预警值', d.lowStock ?? 10),
crmField('销售次数', d.saleCount ?? 0),
crmField('税率', `${d.taxRate ?? 13}%`),
crmTagField('是否易制毒', formatHazardFlag(d.isToxic), getHazardFlagTagType(d.isToxic)),
crmTagField('是否易制爆', formatHazardFlag(d.isExplosive), getHazardFlagTagType(d.isExplosive)),
crmField('创建时间', d.createTime)
]
const priceFields: ViewField[] = [
const fields: ViewField[] = [
{
label: '标准价(元)',
value: formatProductPrice(d.price),
@@ -660,21 +657,38 @@ const productViewSections = computed<ViewSection[]>(() => {
label: '成本价(元)',
value: formatProductPrice(d.costPrice ?? d.cost),
money: true
},
crmField('库存数量', `${d.stock ?? 0} ${formatProductUnit(d.unit)}`),
crmDictField('状态', 'crm_product_status', d.status, dictStore),
crmField('产品分类', d.categoryName),
crmField('规格型号', d.model),
crmField('条码', d.barcode),
crmField('库存预警值', d.lowStock ?? 10),
crmField('销售次数', d.saleCount ?? 0),
crmField('税率', `${d.taxRate ?? 13}%`),
crmTagField('是否易制毒', formatHazardFlag(d.isToxic), getHazardFlagTagType(d.isToxic)),
crmTagField('是否易制爆', formatHazardFlag(d.isExplosive), getHazardFlagTagType(d.isExplosive)),
crmField('创建时间', d.createTime, { long: false })
]
if (d.description) fields.push(crmLongField('产品描述', d.description, { fullSpan: true }))
if (d.remark) fields.push(crmLongField('备注', d.remark, { fullSpan: true }))
return [
{
title: '基本信息',
hideTitleInDense: true,
fieldLayout: 'table',
tableMode: 'quad',
fields
},
{
title: '产品图片',
titleStyle: 'field',
fields: [],
slot: 'images'
}
]
const sections: ViewSection[] = [
{ title: '基本信息', fields: basicFields, fieldLayout: 'table', tableMode: 'triple' },
{ title: '价格信息', fields: priceFields },
{ title: '产品图片', fields: [], slot: 'images' }
]
const extraFields: ViewField[] = []
if (d.description) extraFields.push(crmLongField('产品描述', d.description, { fullSpan: true }))
if (d.remark) extraFields.push(crmLongField('备注', d.remark, { fullSpan: true }))
if (extraFields.length) sections.push({ title: '补充说明', fields: extraFields })
return sections
})
const importDialogVisible = ref(false)
@@ -689,7 +703,12 @@ async function getList() {
page: pagination.page,
pageSize: pagination.pageSize,
}
if (searchForm.keyword) params.keyword = searchForm.keyword
const quickKeyword = quickSearchKeyword.value?.trim()
if (quickKeyword) {
params.keyword = quickKeyword
} else if (searchForm.keyword) {
params.keyword = searchForm.keyword
}
if (searchForm.categoryId) params.categoryId = searchForm.categoryId
if (searchForm.status) params.status = searchForm.status
const res = await fetchProductList(params)
@@ -707,6 +726,7 @@ async function getCategoryOptions() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
@@ -724,6 +744,18 @@ function handleReset() {
searchForm.keyword = ''
searchForm.categoryId = ''
searchForm.status = ''
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
function handleQuickSearch() {
pagination.page = 1
getList()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
}
@@ -928,7 +960,12 @@ async function downloadTemplate() {
async function handleExport() {
try {
const params: Record<string, any> = {}
if (searchForm.keyword) params.keyword = searchForm.keyword
const quickKeyword = quickSearchKeyword.value?.trim()
if (quickKeyword) {
params.keyword = quickKeyword
} else if (searchForm.keyword) {
params.keyword = searchForm.keyword
}
if (searchForm.categoryId) params.categoryId = searchForm.categoryId
if (searchForm.status) params.status = searchForm.status
const res = await fetchExportProducts(params)
@@ -972,6 +1009,7 @@ function resetForm() {
watchDebounced(
searchForm,
() => {
quickSearchKeyword.value = ''
pagination.page = 1
getList()
},
@@ -1008,7 +1046,7 @@ onMounted(() => {
.product-images {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 10px;
}
.product-view-hero-actions {
@@ -1019,3 +1057,91 @@ onMounted(() => {
gap: 8px;
}
</style>
<style lang="scss">
/* 产品查看弹窗:顶部栏背景 + 信息区样式(append-to-body 须非 scoped) */
.crm-product-view-dialog.system-view-dialog--dense,
.el-dialog.crm-product-view-dialog.system-view-dialog--dense {
.view-hero {
padding: 14px 16px 12px;
background: linear-gradient(135deg, #ecfdf5 0%, #f0fdf9 50%, #f8fafc 100%) !important;
border-bottom: 1px solid #d9e8e2;
}
.view-hero-main h2 {
font-size: 17px;
font-weight: 700;
color: #0f172a;
letter-spacing: -0.01em;
}
.view-hero-meta {
font-size: 12px;
color: #64748b;
}
.view-dense-body {
padding: 16px 16px 14px;
background: #fff;
}
.view-kv-table-wrap {
border-color: #eef4f1;
border-radius: 8px;
}
.view-kv-table th {
background: #f8fafc;
}
.view-kv-table tr.row-long td {
font-weight: 400;
color: #475569;
line-height: 1.55;
}
.view-dense-body .view-field-label {
margin: 16px 0 10px;
padding-left: 8px;
border-left: 2px solid #10b981;
font-size: 13px;
font-weight: 600;
color: #475569;
line-height: 1.4;
}
.view-dense-body .view-section:first-child .view-field-label {
margin-top: 0;
}
.product-images {
padding: 10px 12px;
gap: 10px;
background: #f8fafc;
border: 1px solid #eef4f1;
border-radius: 8px;
}
.product-images .el-image {
border-radius: 6px;
border: 1px solid #d9e8e2;
overflow: hidden;
transition: box-shadow 0.15s ease;
}
.product-images .el-image:hover {
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.15);
}
.view-section-slot .view-empty-hint {
margin: 0;
padding: 10px 12px;
font-size: 12px;
color: #94a3b8;
background: #f8fafc;
border: 1px dashed #dcefe8;
border-radius: 8px;
text-align: center;
}
}
</style>
@@ -28,7 +28,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -15,7 +15,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -65,7 +65,6 @@
class="list-table"
stripe
@selection-change="handleSelectionChange"
@row-click="handleRowClick"
:max-height="400"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
@@ -262,13 +261,6 @@ const handleSelectionChange = (selection: ProductVO[]) => {
selectedProducts.value = selection;
};
/** 单击行选中/取消选中产品(点击复选框列时不重复切换) */
const handleRowClick = (row: any, _column: unknown, event: MouseEvent) => {
const target = event.target as HTMLElement
if (target.closest('.el-checkbox')) return
tableRef.value?.toggleRowSelection(row)
}
/** 移除已选产品 */
const handleRemoveSelection = (product: ProductVO) => {
const index = selectedProducts.value.findIndex((p) => p.id === product.id);
@@ -4,8 +4,9 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
class="quote-form-body"
label-position="right"
label-width="100px"
class="crm-dialog-form quote-form-body"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -399,7 +400,9 @@
ref="submitFormRef"
:model="submitForm"
:rules="submitRules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -1697,8 +1700,9 @@ onMounted(async () => {
.quote-type-item :deep(.el-form-item__label) {
display: flex;
align-items: center;
min-height: 56px;
line-height: 56px;
min-height: auto;
line-height: 1.45;
height: auto !important;
}
.quote-type-item :deep(.el-form-item__content) {
@@ -1,6 +1,6 @@
<template>
<el-dialog v-model="visible" title="生成分享链接" width="600px" :close-on-click-modal="false" @close="handleClose">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
<el-form-item label="有效期" prop="validityDays">
<el-select v-model="form.validityDays" placeholder="请选择有效期" style="width: 100%">
<el-option label="3天" :value="3" />
@@ -4,7 +4,7 @@
将报价单转换为合同,系统将自动复制报价信息、产品明细、付款条款等内容到新合同
</el-alert>
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px" class="crm-dialog-form">
<el-divider content-position="left">报价单信息</el-divider>
<el-descriptions :column="2" border size="small" class="mb-4">
@@ -106,13 +106,6 @@
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button class="btn-ghost" @click="handleClose">
<el-icon><Close /></el-icon>关闭
</el-button>
</div>
</template>
</el-dialog>
</template>
@@ -122,7 +115,6 @@ const dictStore = useDictStore()
import DictTag from '@/components/DictTag/index.vue'
import ListPagination from '@/components/ListPagination/index.vue'
import { $modal } from '@/utils/modal'
import { Close } from '@element-plus/icons-vue'
import { ref, computed, getCurrentInstance, toRefs, type ComponentInternalInstance } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { queryVersions, type QuoteVO } from '@/api/crm/quote';
+1 -1
View File
@@ -29,7 +29,7 @@
<p v-if="quote?.quoteNo" class="detail-subtitle">{{ quote.quoteNo }} — {{ quote.quoteTitle }}</p>
<el-tabs v-model="activeTab" class="panel-pill-tabs panel-pill-tabs--lg">
<el-tabs v-model="activeTab" class="panel-tabs">
<!-- 基本信息 -->
<el-tab-pane label="基本信息" name="basic">
<el-descriptions :column="2" border>
+26 -43
View File
@@ -7,7 +7,7 @@
<el-tabs
v-model="activeView"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleViewChange"
>
<el-tab-pane label="我的报价" name="my" />
@@ -39,43 +39,25 @@
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="queryParams.customerName" placeholder="客户名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
</div>
<div v-show="filterExpanded" id="quote-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="报价类型" prop="quoteType">
<el-select v-model="queryParams.quoteType" placeholder="全部" clearable>
<el-option label="销售" value="sales" />
<el-option label="采购" value="purchase" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_quote_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
</div>
<el-form-item label="报价类型" prop="quoteType">
<el-select v-model="queryParams.quoteType" placeholder="全部" clearable>
<el-option label="销售" value="sales" />
<el-option label="采购" value="purchase" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable>
<el-option
v-for="opt in dictStore.getOptions('crm_quote_status')"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="quote-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="resetQuery">
<el-icon><RefreshLeft /></el-icon>重置
@@ -456,7 +438,9 @@
ref="submitFormRef"
:model="submitForm"
:rules="submitRules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -496,7 +480,7 @@
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
>
<el-form ref="shareFormRef" :model="shareForm" :rules="shareRules" label-position="top">
<el-form ref="shareFormRef" :model="shareForm" :rules="shareRules" label-position="right" label-width="100px" class="crm-dialog-form">
<el-form-item label="有效期(天)" prop="validityDays">
<el-input-number v-model="shareForm.validityDays" :min="1" :max="365" placeholder="请输入有效期" style="width: 200px" />
<span class="form-label-tip">不填则永久有效</span>
@@ -531,7 +515,7 @@
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
>
<el-form ref="versionFormRef" :model="versionForm" :rules="versionRules" label-position="top">
<el-form ref="versionFormRef" :model="versionForm" :rules="versionRules" label-position="right" label-width="100px" class="crm-dialog-form">
<el-form-item label="版本号" prop="version">
<el-input v-model="versionForm.version" placeholder="请输入版本号,如V1.1、V2.0" style="width: 200px" />
</el-form-item>
@@ -661,7 +645,9 @@
ref="approvalFormRef"
:model="approvalForm"
:rules="approvalRules"
label-position="top"
label-position="right"
label-width="100px"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -727,8 +713,6 @@
<script setup lang="ts" name="QuoteIndex">
import {
ArrowDownBold,
ArrowUpBold,
Check,
Delete,
CircleClose,
@@ -951,7 +935,6 @@ const quoteList = ref<QuoteVO[]>([]);
const loading = ref(true);
const total = ref(0);
const expandLoadingMap = reactive<Record<string, boolean>>({});
const filterExpanded = ref(false);
const selectedRows = ref<QuoteVO[]>([]);
const exportLoading = ref(false);
const single = ref(true);
+37 -51
View File
@@ -2,7 +2,7 @@
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">出入库管理</h2>
<h2 class="title-bar-heading">地磅出入库</h2>
</div>
<div class="filter-block">
@@ -34,30 +34,12 @@
<el-option v-for="item in statusOptions" :key="item" :label="item" :value="item" />
</el-select>
</el-form-item>
</div>
<div v-show="filterExpanded" id="db-inv-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="车牌号">
<el-input v-model="filters.vehicleNo" placeholder="车牌号" clearable @keyup.enter="handleSearch" />
</el-form-item>
</div>
<el-form-item label="车牌号">
<el-input v-model="filters.vehicleNo" placeholder="车牌号" clearable @keyup.enter="handleSearch" />
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="db-inv-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleResetFilters">
<el-icon><RefreshLeft /></el-icon>
@@ -123,18 +105,12 @@
本周
</el-button>
</div>
<el-input
<ListQuickSearchInput
v-model="searchKeyword"
class="db-toolbar-quick-search-input"
placeholder="计划号 / 单号 / 往来单位 / 产品 / 车牌"
clearable
@keyup.enter="handleQuickSearch"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
>
<template #suffix>
<el-icon class="db-quick-search-icon" @click="handleQuickSearch"><Search /></el-icon>
</template>
</el-input>
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增记录
@@ -173,11 +149,7 @@
!canDeleteByCreator(selectedRows[0])
"
:title="
selectedRows.length === 1 && !canDeleteRecord(selectedRows[0])
? '仅支持删除当天及之后日期的记录'
: selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0])
? CREATOR_DELETE_TIP
: ''
selectedRows.length === 1 ? getDeleteRecordDisabledTitle(selectedRows[0]) : ''
"
@click="handleToolbarDelete"
>
@@ -428,7 +400,7 @@
<el-dialog
v-model="modalOpen"
:title="editingRecord ? '修改记录' : '新增记录'"
width="640px"
width="740px"
align-center
destroy-on-close
:close-on-click-modal="false"
@@ -440,8 +412,9 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="form-grid-2"
label-position="right"
label-width="100px"
class="db-dialog-form form-grid-2"
scroll-to-error
>
<el-form-item label="日期" prop="date">
@@ -539,13 +512,7 @@
class="btn-ghost"
type="danger"
:disabled="!canDeleteRecord(viewingRecord) || !canDeleteByCreator(viewingRecord)"
:title="
!canDeleteRecord(viewingRecord)
? '仅支持删除当天及之后日期的记录'
: canDeleteByCreator(viewingRecord)
? ''
: CREATOR_DELETE_TIP
"
:title="viewingRecord ? getDeleteRecordDisabledTitle(viewingRecord) : ''"
@click="handleViewDelete"
>
<el-icon><Delete /></el-icon>
@@ -565,8 +532,6 @@
*/
import type { TableColumnCtx } from 'element-plus'
import {
ArrowDownBold,
ArrowUpBold,
Check,
Close,
CopyDocument,
@@ -579,9 +544,11 @@
Search,
} from '@element-plus/icons-vue'
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
import DbInventoryRelatedViewDialog from '@/components/business/db/DbInventoryRelatedViewDialog.vue'
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
@@ -657,6 +624,13 @@
const relatedData = ref<InventoryRelatedData>({ plan: null, weighing: null })
const viewingRecord = ref<InventoryRecord | null>(null)
async function refreshRelatedViewIfOpen(editedId?: string) {
if (!relatedModalOpen.value || !viewingRecord.value?.id) return
if (editedId && viewingRecord.value.id !== editedId) return
syncViewRowFromFlatList(viewingRecord, allRecords.value)
relatedData.value = await inventoryApi.getRelated(viewingRecord.value.id)
}
const filters = reactive({
batchNo: '',
businessType: '' as '' | '入库' | '出库',
@@ -664,7 +638,6 @@
vehicleNo: ''
})
const filterExpanded = ref(false)
const businessTypeOptions: Array<'入库' | '出库'> = ['入库', '出库']
const statusOptions = ['待处理', '待入库', '待出库', '待三方填写', '已完成']
@@ -995,12 +968,23 @@
}
const canDeleteRecord = (row: InventoryRecord) => {
if (row.status === '已完成') return false
const dateStr = row.date || ''
if (!dateStr) return false
return dateStr >= formatYmd(new Date())
}
const getDeleteRecordDisabledTitle = (row: InventoryRecord) => {
if (row.status === '已完成') return '已完成的记录不能删除'
if (!canDeleteByCreator(row)) return CREATOR_DELETE_TIP
return '仅支持删除当天及之后日期的记录,历史数据不可删除'
}
const handleDelete = async (row: InventoryRecord) => {
if (row.status === '已完成') {
$modal.msgWarning('已完成的记录不能删除')
return
}
if (!canDeleteRecord(row)) {
$modal.msgWarning('仅支持删除当天及之后日期的记录,历史数据不可删除')
return
@@ -1068,6 +1052,7 @@
}
if (editingRecord.value) {
const editedId = editingRecord.value.id
const updateData = cleanData({
date: formData.date,
partnerCompany: formData.partnerCompany,
@@ -1086,7 +1071,8 @@
openedFromPlan.value = false
router.replace('/db/plans')
} else {
loadData()
await loadData()
await refreshRelatedViewIfOpen(editedId)
}
} else {
const createData: InventoryCreate = {
+47 -18
View File
@@ -2,7 +2,7 @@
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">计划管理</h2>
<h2 class="title-bar-heading">地磅计划</h2>
</div>
<div class="filter-block">
@@ -119,18 +119,12 @@
本周
</el-button>
</div>
<el-input
<ListQuickSearchInput
v-model="searchKeyword"
class="db-toolbar-quick-search-input"
placeholder="往来单位 / 计划号 / 产品"
clearable
@keyup.enter="handleQuickSearch"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
>
<template #suffix>
<el-icon class="db-quick-search-icon" @click="handleQuickSearch"><Search /></el-icon>
</template>
</el-input>
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增计划
@@ -154,8 +148,12 @@
<el-button
class="btn-ghost"
type="danger"
:disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
:title="selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0]) ? CREATOR_DELETE_TIP : ''"
:disabled="
selectedRows.length !== 1 ||
!canDeletePlanRecord(selectedRows[0]) ||
!canDeleteByCreator(selectedRows[0])
"
:title="selectedRows.length === 1 ? getDeletePlanDisabledTitle(selectedRows[0]) : ''"
@click="handleToolbarDelete"
>
<el-icon><Delete /></el-icon>
@@ -356,8 +354,9 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="form-grid-2"
label-position="right"
label-width="100px"
class="db-dialog-form form-grid-2"
scroll-to-error
>
<el-form-item label="计划号" prop="planNo">
@@ -474,8 +473,8 @@
<el-button
class="btn-ghost"
type="danger"
:disabled="!canDeleteByCreator(viewingRecord)"
:title="canDeleteByCreator(viewingRecord) ? '' : CREATOR_DELETE_TIP"
:disabled="!canDeletePlanRecord(viewingRecord) || !canDeleteByCreator(viewingRecord)"
:title="viewingRecord ? getDeletePlanDisabledTitle(viewingRecord) : ''"
@click="handleViewDelete"
>
<el-icon><Delete /></el-icon>
@@ -504,6 +503,7 @@
Search,
} from '@element-plus/icons-vue'
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
@@ -511,6 +511,7 @@
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { $modal } from '@/utils/modal'
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
import {
planApi,
type Plan,
@@ -832,6 +833,14 @@
const loadingRelated = ref(false)
const viewingRecord = ref<Plan | null>(null)
async function refreshRelatedViewIfOpen(editedId?: string) {
if (!relatedModalOpen.value || !viewingRecord.value?.id) return
if (editedId && viewingRecord.value.id !== editedId) return
const records = await fetchFilteredPlans()
syncViewRowFromFlatList(viewingRecord, records)
relatedData.value = await planApi.getRelated(viewingRecord.value.id)
}
const handleView = async (record: Plan) => {
viewingRecord.value = record
loadingRelated.value = true
@@ -875,7 +884,23 @@
relatedModalOpen.value = false
}
const executionCompletedStatuses = ['已入库', '已出库']
const canDeletePlanRecord = (row: Plan) => {
return !row.executionStatus || !executionCompletedStatuses.includes(row.executionStatus)
}
const getDeletePlanDisabledTitle = (row: Plan) => {
if (!canDeletePlanRecord(row)) return '已完成的计划不能删除'
if (!canDeleteByCreator(row)) return CREATOR_DELETE_TIP
return ''
}
const handleDelete = async (row: Plan) => {
if (!canDeletePlanRecord(row)) {
$modal.msgWarning('已完成的计划不能删除')
return
}
if (!canDeleteByCreator(row)) {
$modal.msgWarning(CREATOR_DELETE_TIP)
return
@@ -960,14 +985,18 @@
}
if (editingPlan.value) {
const editedId = editingPlan.value.id
await planApi.update(editingPlan.value.id, payload as PlanUpdate)
$modal.msgSuccess('更新成功')
modalOpen.value = false
await loadData()
await refreshRelatedViewIfOpen(editedId)
} else {
await planApi.create(payload as PlanCreate)
$modal.msgSuccess('创建成功')
modalOpen.value = false
loadData()
}
modalOpen.value = false
loadData()
} catch (error) {
console.error('保存计划失败', error)
} finally {
+30 -39
View File
@@ -32,30 +32,12 @@
<el-form-item label="供货单位">
<el-input v-model="filters.supplier" placeholder="供货单位" clearable @keyup.enter="handleSearch" />
</el-form-item>
</div>
<div v-show="filterExpanded" id="db-weigh-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="收货单位">
<el-input v-model="filters.consignee" placeholder="收货单位" clearable @keyup.enter="handleSearch" />
</el-form-item>
</div>
<el-form-item label="收货单位">
<el-input v-model="filters.consignee" placeholder="收货单位" clearable @keyup.enter="handleSearch" />
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="db-weigh-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleResetFilters">
<el-icon><RefreshLeft /></el-icon>
@@ -121,25 +103,19 @@
本周
</el-button>
</div>
<el-input
<ListQuickSearchInput
v-model="searchKeyword"
class="db-toolbar-quick-search-input"
placeholder="车牌号 / 货物 / 计划号 / 出入库单号"
clearable
@keyup.enter="handleQuickSearch"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
>
<template #suffix>
<el-icon class="db-quick-search-icon" @click="handleQuickSearch"><Search /></el-icon>
</template>
</el-input>
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增地磅单
</el-button>
<el-button
class="btn-ghost"
:disabled="selectedRows.length !== 1"
:disabled="selectedRows.length !== 1 || selectedRows[0]?.isVoided === 1"
@click="handleToolbarEdit"
>
<el-icon><Edit /></el-icon>
@@ -395,8 +371,9 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
class="form-grid-2"
label-position="right"
label-width="100px"
class="db-dialog-form form-grid-2"
scroll-to-error
>
<el-form-item label="车牌号" prop="vehicleNo">
@@ -522,7 +499,12 @@
:sections="relatedViewSections"
>
<template v-if="viewingRecord" #hero-actions>
<el-button class="btn-main" type="primary" @click="handleEdit(viewingRecord)">
<el-button
v-if="viewingRecord.isVoided === 0"
class="btn-main"
type="primary"
@click="handleEdit(viewingRecord)"
>
<el-icon><Edit /></el-icon>
编辑
</el-button>
@@ -548,8 +530,6 @@
*/
import type { TableColumnCtx } from 'element-plus'
import {
ArrowDownBold,
ArrowUpBold,
Check,
Close,
Download,
@@ -559,6 +539,7 @@
Search,
} from '@element-plus/icons-vue'
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
@@ -566,6 +547,7 @@
import { buildWeighingRelatedSections } from '@/components/business/db/db-related-view'
import PlVehicleSelect from '@/components/business/pl/PlVehicleSelect.vue'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
import { $modal } from '@/utils/modal'
import { inventoryApi } from '@/api/db/inventory'
import { planApi } from '@/api/db/plans'
@@ -627,7 +609,6 @@
businessType: '' as '' | '入库' | '出库'
})
const filterExpanded = ref(false)
const businessTypeOptions: Array<'入库' | '出库'> = ['入库', '出库']
const pad2 = (n: number) => String(n).padStart(2, '0')
@@ -645,6 +626,13 @@
const relatedData = ref<WeighingRelatedData>({ plan: null, inventory: null })
const viewingRecord = ref<WeighingRecord | null>(null)
async function refreshRelatedViewIfOpen(editedId?: string) {
if (!relatedModalOpen.value || !viewingRecord.value?.id) return
if (editedId && viewingRecord.value.id !== editedId) return
syncViewRowFromFlatList(viewingRecord, allData.value)
relatedData.value = await weighingApi.getRelated(viewingRecord.value.id)
}
const relatedViewSections = computed(() => buildWeighingRelatedSections(relatedData.value))
const formRef = ref()
@@ -1013,6 +1001,7 @@
if (!valid) return
saving.value = true
try {
let editedId: string | undefined
const hasGrossWeight = formData.grossWeight && formData.grossWeight > 0
const hasTareWeight = formData.tareWeight && formData.tareWeight > 0
if (hasGrossWeight && hasTareWeight) {
@@ -1045,6 +1034,7 @@
}
if (editingRecord.value) {
editedId = editingRecord.value.id
await weighingApi.update(editingRecord.value.id, {
productId: formData.productId,
vehicleNo: submitData.vehicleNo,
@@ -1073,7 +1063,8 @@
$modal.msgSuccess('保存成功')
}
modalOpen.value = false
loadData()
await loadData()
if (editedId) await refreshRelatedViewIfOpen(editedId)
} catch (error) {
console.error('保存地磅单失败', error)
} finally {
+19 -58
View File
@@ -28,9 +28,7 @@
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" label-position="top">
<p class="filter-hint"
>支持按订单状态、配送状态、支付状态等维度快速筛选;可展开更多条件进行精细检索。</p
>
<p class="filter-hint">支持按订单状态、配送状态、支付状态等维度快速筛选。</p>
<div class="filter-grid order-filter-main-grid">
<el-form-item label="关键词">
<el-input
@@ -64,32 +62,17 @@
<el-option label="已支付" value="paid" />
</el-select>
</el-form-item>
</div>
<div v-show="filterExpanded" class="filter-extra">
<div class="filter-grid order-filter-extra-grid">
<el-form-item label="支付方式">
<el-select v-model="searchForm.paymentMethod" placeholder="全部" clearable>
<el-option label="全部" value="" />
<el-option label="线上支付" value="online" />
<el-option label="线下支付" value="offline" />
</el-select>
</el-form-item>
</div>
<el-form-item label="支付方式">
<el-select v-model="searchForm.paymentMethod" placeholder="全部" clearable>
<el-option label="全部" value="" />
<el-option label="线上支付" value="online" />
<el-option label="线下支付" value="offline" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions">
<el-button
class="btn-link btn-filter-toggle"
:aria-expanded="String(filterExpanded)"
@click="filterExpanded = !filterExpanded"
>
<el-icon class="btn-filter-toggle-ic">
<ArrowDown v-if="!filterExpanded" />
<ArrowUp v-else />
</el-icon>
{{ filterExpanded ? '收起更多条件' : '展开更多条件' }}
</el-button>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
@@ -111,8 +94,11 @@
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@expand-change="handleOrderRowExpand"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -1684,11 +1670,6 @@
</el-table>
<el-empty v-else-if="!orderPriceVersionLoading" description="暂无明细" />
</div>
<template #footer>
<el-button class="btn-ghost" @click="orderPriceVersionVisible = false"
><el-icon><Close /></el-icon>关闭</el-button
>
</template>
</el-dialog>
</div>
</template>
@@ -1708,8 +1689,6 @@
import { ElMessage, ElMessageBox } from 'element-plus'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
ArrowDown,
ArrowUp,
Check,
Close,
Plus,
@@ -1795,6 +1774,7 @@
const expandedRowLoadingMap = ref<Record<string, boolean>>({})
const tableData = ref<any[]>([])
const loading = ref(false)
const selectedRows = ref<any[]>([])
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
const regionList = ref<any[]>([])
const activeRegionId = ref('')
@@ -1810,8 +1790,6 @@
const allow = new Set(allowedRegionIds.value)
return regionList.value.filter((r: any) => allow.has(String(r.id)))
})
const filterExpanded = ref(false)
const viewVisible = ref(false)
const detailLoading = ref(false)
const activeTab = ref('order')
@@ -2295,6 +2273,10 @@
}
}
const handleSelectionChange = (rows: any[]) => {
selectedRows.value = rows
}
const handleOrderRowExpand = async (row: any, expandedRows: any[]) => {
const expanded = expandedRows.some((r) => r.id === row.id)
if (!expanded) return
@@ -2429,7 +2411,6 @@
paymentMethod: '',
regionId: activeRegionId.value || ''
})
filterExpanded.value = false
pagination.page = 1
loadData()
}
@@ -2940,20 +2921,14 @@
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.order-filter-extra-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 1100px) {
.order-filter-main-grid,
.order-filter-extra-grid {
.order-filter-main-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 560px) {
.order-filter-main-grid,
.order-filter-extra-grid {
.order-filter-main-grid {
grid-template-columns: 1fr;
}
}
@@ -2971,12 +2946,6 @@
line-height: 1.2;
}
.filter-extra {
padding-top: 10px;
margin-top: 8px;
border-top: 0; /* 设计图:筛选内部不加分割线 */
}
.filter-actions {
display: flex;
flex-wrap: wrap;
@@ -3029,14 +2998,6 @@
pointer-events: none;
}
.btn-filter-toggle {
height: 36px;
padding: 0 8px;
}
.btn-filter-toggle-ic {
margin-right: 4px;
}
.order-row-expand-wrap {
padding: 0;
background: #f8fafc;
+4 -1
View File
@@ -6,6 +6,10 @@
</div>
<div id="app-portal-body">
<aside id="app-sidebar">
<ArtSidebarMenu />
</aside>
<main id="app-workspace">
<div id="app-content">
<ArtPageContent />
@@ -57,7 +61,6 @@
if (menuType.value !== MenuTypeEnum.PORTAL) {
settingStore.switchMenuLayouts(MenuTypeEnum.PORTAL)
}
settingStore.setMenuOpen(true)
// 门户布局强制严格单页(忽略 localStorage 中历史 showWorkTab=true)
settingStore.setWorkTab(false)
worktabStore.enforceSinglePageTabs()
+43 -5
View File
@@ -14,17 +14,21 @@
#app-portal-body {
display: flex;
flex: 1;
flex-direction: column;
flex-direction: row;
min-height: 0;
height: calc(100vh - 56px);
overflow: hidden;
}
#app-sidebar {
flex-shrink: 0;
height: 100%;
}
#app-workspace {
display: flex;
flex: 1;
flex-direction: column;
width: 100%;
min-width: 0;
height: 100%;
overflow: hidden;
@@ -46,7 +50,7 @@
box-sizing: border-box;
width: 100%;
margin: 0;
padding: var(--mint-content-padding);
padding: 0 10px;
.page-content {
position: relative;
@@ -59,6 +63,24 @@
box-shadow: none !important;
}
}
/* 页面主标题已移至门户顶栏居中展示 */
:deep(
.page-container .panel-top:not(.panel-top-with-actions):not(.user-dept-aside__head):not(
.storage-aside__head
):not(.storage-content-header),
.console-container .panel-top
) {
display: none;
}
:deep(.panel-top.panel-top-with-actions .title-bar-heading) {
display: none;
}
:deep(.panel-top.panel-top-with-actions:not(:has(.panel-top-main))) {
justify-content: flex-end;
}
}
}
@@ -72,10 +94,26 @@
@media only screen and (width <= 800px) {
.app-layout-portal {
#app-portal-body {
position: relative;
}
#app-sidebar {
position: fixed;
top: 56px;
left: 0;
z-index: 300;
height: calc(100vh - 56px);
}
#app-workspace {
width: 100%;
}
#app-content {
:deep(.layout-content) {
width: 100%;
padding: 16px;
padding: 0 10px;
}
}
}
@@ -86,7 +124,7 @@
#app-content {
:deep(.layout-content) {
width: 100%;
padding: 12px;
padding: 0 10px;
}
}
}
+62 -21
View File
@@ -51,6 +51,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="承运商名称/联系人/电话"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增承运商
</el-button>
@@ -216,12 +222,12 @@
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="720px"
width="960px"
align-center
class="dialog-form-layout"
@close="handleDialogClose"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="right" label-width="100px" class="pl-dialog-form">
<div class="form-grid-2">
<el-form-item label="承运商名称" prop="carrierName">
<el-input v-model="formData.carrierName" placeholder="请输入承运商名称" maxlength="200" />
@@ -238,20 +244,20 @@
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="formData.contactPhone" placeholder="请输入联系电话" maxlength="32" />
</el-form-item>
<el-form-item label="备注" class="form-col-span-2" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
maxlength="512"
/>
</el-form-item>
</div>
<el-form-item label="备注" class="form-col-span-2" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
maxlength="512"
/>
</el-form-item>
<el-divider content-position="left">资质附件</el-divider>
<div class="form-grid-2">
<el-form-item label="营业执照" prop="businessLicenseUrls">
<el-form-item label="营业执照" prop="businessLicenseUrls" class="form-col-span-2">
<AttachmentUploadField
ref="businessLicenseUploadRef"
v-model="formData.businessLicenseUrls"
@@ -262,7 +268,7 @@
@update:model-value="handleCertFieldChange('businessLicenseUrls')"
/>
</el-form-item>
<el-form-item label="道路经营许可证" prop="roadTransportLicenseUrls">
<el-form-item label="道路经营许可证" prop="roadTransportLicenseUrls" class="form-col-span-2">
<AttachmentUploadField
ref="roadTransportLicenseUploadRef"
v-model="formData.roadTransportLicenseUrls"
@@ -307,7 +313,11 @@
</template>
</el-dialog>
<PlCarrierViewDialog v-model:visible="viewVisible" :carrier-id="viewCarrierId">
<PlCarrierViewDialog
ref="viewDialogRef"
v-model:visible="viewVisible"
:carrier-id="viewCarrierId"
>
<template v-if="viewingRow" #hero-actions>
<el-button class="btn-main" type="primary" @click="handleEdit(viewingRow)">
<el-icon><Edit /></el-icon>编辑
@@ -340,6 +350,7 @@ 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 ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { pickBxPageResult } from '@/utils/http/bx'
@@ -351,6 +362,7 @@ import {
} from '@/utils/pl-display'
const loading = ref(false)
const quickSearchKeyword = ref('')
const listColumnDefs = [
{ prop: 'contactName', label: '联系人' },
@@ -384,6 +396,7 @@ const isEdit = ref(false)
const viewVisible = ref(false)
const viewCarrierId = ref('')
const viewingRow = ref<Carrier | null>(null)
const viewDialogRef = ref<InstanceType<typeof PlCarrierViewDialog>>()
const searchForm = reactive({
q: '',
@@ -446,9 +459,6 @@ const createQualificationRules = {
},
trigger: 'change'
}
],
transportAgreementExpireDate: [
{ required: true, message: '请选择协议有效期', trigger: 'change' }
]
}
@@ -473,7 +483,7 @@ async function fetchData() {
loading.value = true
try {
const res = await carrierApi.list({
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
status: searchForm.status || undefined,
auditStatus: searchForm.auditStatus || undefined,
page: pagination.page,
@@ -490,6 +500,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -498,11 +509,23 @@ function handleReset() {
searchForm.q = ''
searchForm.status = ''
searchForm.auditStatus = ''
quickSearchKeyword.value = ''
selectedRows.value = []
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handlePageChange() {
fetchData()
}
@@ -516,6 +539,18 @@ function canApproveCarrier(row: Carrier) {
return row.auditStatus !== 'approved' && row.qualificationComplete
}
function syncViewingRowFromList() {
if (!viewingRow.value?.id) return
const updated = carrierList.value.find(row => row.id === viewingRow.value!.id)
if (updated) viewingRow.value = updated
}
async function refreshViewIfOpen() {
if (!viewVisible.value || !viewCarrierId.value) return
syncViewingRowFromList()
await viewDialogRef.value?.reload()
}
async function handleApprove(row: Carrier) {
if (!canApproveCarrier(row)) {
ElMessage.warning('承运商资质不完整,无法审核通过')
@@ -530,7 +565,8 @@ async function handleApprove(row: Carrier) {
try {
await carrierApi.approve(row.id)
ElMessage.success('审核通过')
fetchData()
await fetchData()
await refreshViewIfOpen()
} finally {
auditLoadingMap[row.id] = false
}
@@ -641,7 +677,9 @@ async function handleSubmit() {
businessLicenseUrls: formData.businessLicenseUrls,
roadTransportLicenseUrls: formData.roadTransportLicenseUrls,
transportAgreementUrls: formData.transportAgreementUrls,
transportAgreementExpireDate: formData.transportAgreementExpireDate
...(formData.transportAgreementExpireDate
? { transportAgreementExpireDate: formData.transportAgreementExpireDate }
: {})
})
}
if (isEdit.value) {
@@ -651,7 +689,10 @@ async function handleSubmit() {
}
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
dialogVisible.value = false
fetchData()
await fetchData()
if (isEdit.value) {
await refreshViewIfOpen()
}
} finally {
submitLoading.value = false
}
+33 -11
View File
@@ -38,6 +38,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="司机姓名/手机号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增司机
</el-button>
@@ -178,7 +184,7 @@
class="dialog-form-layout"
@close="handleDialogClose"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="right" label-width="100px" class="pl-dialog-form">
<div class="form-grid-2">
<el-form-item label="姓名" prop="driverName">
<el-input v-model="formData.driverName" placeholder="请输入姓名" maxlength="100" />
@@ -225,16 +231,16 @@
style="width: 100%"
/>
</el-form-item>
<el-form-item label="备注" class="form-col-span-2" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
maxlength="512"
/>
</el-form-item>
</div>
<el-form-item label="备注" class="form-col-span-2" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
maxlength="512"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
@@ -280,6 +286,7 @@
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
import { driverApi, type Driver } from '@/api/pl'
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
@@ -324,6 +331,8 @@ const isEdit = ref(false)
const viewVisible = ref(false)
const viewRow = ref<Driver | null>(null)
const quickSearchKeyword = ref('')
const searchForm = reactive({
q: ''
})
@@ -411,7 +420,7 @@ async function fetchData() {
loading.value = true
try {
const res = await driverApi.list({
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
@@ -426,17 +435,30 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleReset() {
searchForm.q = ''
quickSearchKeyword.value = ''
selectedRows.value = []
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handlePageChange() {
fetchData()
}
@@ -8,7 +8,7 @@
<el-tabs
v-model="activeTab"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleTabChange"
>
<el-tab-pane label="待审批" name="pending">
@@ -8,7 +8,7 @@
<el-tabs
v-model="activeTab"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleTabChange"
>
<el-tab-pane label="待发放" name="disburse">
@@ -33,14 +33,37 @@
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">已选 <span class="sel-num">0</span> 项</div>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<div class="list-toolbar-selected">已选 <span class="sel-num">{{ selectedRows.length }}</span> 项</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="结算单号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<el-table class="list-table" :data="settlementList" v-loading="loading" stripe row-key="id">
<el-table
class="list-table"
:data="settlementList"
v-loading="loading"
stripe
row-key="id"
@row-dblclick="handleView"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -54,24 +77,60 @@
<el-link type="primary" @click="handleView(row)">{{ row.settlementNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="totalFreight" label="运费合计(元)" min-width="130" align="right">
<el-table-column
v-if="isColumnVisible('totalFreight')"
prop="totalFreight"
label="运费合计(元)"
min-width="130"
align="right"
>
<template #default="{ row }">{{ formatAmount(row.totalFreight) }}</template>
</el-table-column>
<el-table-column prop="planCount" label="关联计划数" min-width="110" align="center" />
<el-table-column prop="licensePlateSummary" label="车牌摘要" min-width="140" show-overflow-tooltip />
<el-table-column prop="settlementStatus" label="结算状态" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('planCount')"
prop="planCount"
label="关联计划数"
min-width="110"
align="center"
/>
<el-table-column
v-if="isColumnVisible('licensePlateSummary')"
prop="licensePlateSummary"
label="车牌摘要"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('settlementStatus')"
prop="settlementStatus"
label="结算状态"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getSettlementStatusTagType(row.settlementStatus)" size="small">
{{ getSettlementStatusLabel(row.settlementStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="approvalFinishedTime" label="审批完成时间" min-width="160" align="center">
<el-table-column
v-if="isColumnVisible('approvalFinishedTime')"
prop="approvalFinishedTime"
label="审批完成时间"
min-width="160"
align="center"
>
<template #default="{ row }">{{
row.approvalFinishedTime ? formatDateTime(row.approvalFinishedTime) : '—'
}}</template>
</el-table-column>
<el-table-column prop="submitterName" label="提交人" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('submitterName')"
prop="submitterName"
label="提交人"
min-width="100"
align="center"
>
<template #default="{ row }">{{ row.submitterName || '—' }}</template>
</el-table-column>
</el-table>
@@ -94,7 +153,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { RefreshLeft, Search } from '@element-plus/icons-vue'
import { freightApi, type FreightSettlement } from '@/api/pl'
import PlFreightSettlementViewDialog from '@/components/business/pl-freight-settlement-view-dialog/index.vue'
@@ -105,9 +166,28 @@ import {
import { formatMoneyDisplay } from '@/utils/money-format'
import { formatDateTime } from '@/utils/format'
const listColumnDefs = [
{ prop: 'totalFreight', label: '运费合计(元)' },
{ prop: 'planCount', label: '关联计划数' },
{ prop: 'licensePlateSummary', label: '车牌摘要' },
{ prop: 'settlementStatus', label: '结算状态' },
{ prop: 'approvalFinishedTime', label: '审批完成时间' },
{ prop: 'submitterName', label: '提交人' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-freight-approval-done-columns', listColumnDefs)
const loading = ref(false)
const selectedRows = ref<FreightSettlement[]>([])
const settlementList = ref<FreightSettlement[]>([])
const viewDialogRef = ref<InstanceType<typeof PlFreightSettlementViewDialog>>()
const quickSearchKeyword = ref('')
const searchForm = reactive({
q: '',
@@ -120,6 +200,10 @@ const pagination = reactive({
total: 0
})
function handleSelectionChange(rows: FreightSettlement[]) {
selectedRows.value = rows
}
function formatAmount(amount: number) {
if (!amount) return '¥0.00'
return formatMoneyDisplay(amount)
@@ -130,7 +214,7 @@ async function fetchData() {
try {
const res = await freightApi.list({
tab: 'approval_done',
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
settlementStatus: searchForm.settlementStatus || undefined,
page: pagination.page,
pageSize: pagination.pageSize
@@ -144,6 +228,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -151,6 +236,18 @@ function handleSearch() {
function handleReset() {
searchForm.q = ''
searchForm.settlementStatus = ''
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -30,6 +30,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="结算单号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
class="btn-main"
type="primary"
@@ -54,7 +60,15 @@
>
<el-icon><CircleClose /></el-icon>驳回
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -83,18 +97,54 @@
<el-link type="primary" @click="handleView(row)">{{ row.settlementNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="totalFreight" label="运费合计(元)" min-width="130" align="right">
<el-table-column
v-if="isColumnVisible('totalFreight')"
prop="totalFreight"
label="运费合计(元)"
min-width="130"
align="right"
>
<template #default="{ row }">{{ formatAmount(row.totalFreight) }}</template>
</el-table-column>
<el-table-column prop="planCount" label="关联计划数" min-width="110" align="center" />
<el-table-column prop="licensePlateSummary" label="车牌摘要" min-width="140" show-overflow-tooltip />
<el-table-column prop="currentApproverName" label="当前审批人" min-width="110" align="center">
<el-table-column
v-if="isColumnVisible('planCount')"
prop="planCount"
label="关联计划数"
min-width="110"
align="center"
/>
<el-table-column
v-if="isColumnVisible('licensePlateSummary')"
prop="licensePlateSummary"
label="车牌摘要"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('currentApproverName')"
prop="currentApproverName"
label="当前审批人"
min-width="110"
align="center"
>
<template #default="{ row }">{{ row.currentApproverName || '—' }}</template>
</el-table-column>
<el-table-column prop="submitterName" label="提交人" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('submitterName')"
prop="submitterName"
label="提交人"
min-width="100"
align="center"
>
<template #default="{ row }">{{ row.submitterName || '—' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="提交时间" min-width="160" align="center">
<el-table-column
v-if="isColumnVisible('createTime')"
prop="createTime"
label="提交时间"
min-width="160"
align="center"
>
<template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
</el-table-column>
</el-table>
@@ -132,12 +182,14 @@
</el-button>
</template>
</PlFreightSettlementViewDialog>
<PlFreightApprovalDialog ref="approvalDialogRef" @success="fetchData" />
<PlFreightApprovalDialog ref="approvalDialogRef" @success="handleMutationSuccess" />
</div>
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { CircleCheck, CircleClose, RefreshLeft, Search } from '@element-plus/icons-vue'
import { freightApi, type FreightSettlement } from '@/api/pl'
import PlFreightApprovalDialog from '@/components/business/pl-freight-approval-dialog/index.vue'
@@ -145,12 +197,30 @@ import PlFreightSettlementViewDialog from '@/components/business/pl-freight-sett
import { formatMoneyDisplay } from '@/utils/money-format'
import { formatDateTime } from '@/utils/format'
const listColumnDefs = [
{ prop: 'totalFreight', label: '运费合计(元)' },
{ prop: 'planCount', label: '关联计划数' },
{ prop: 'licensePlateSummary', label: '车牌摘要' },
{ prop: 'currentApproverName', label: '当前审批人' },
{ prop: 'submitterName', label: '提交人' },
{ prop: 'createTime', label: '提交时间' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-freight-approval-pending-columns', listColumnDefs)
const loading = ref(false)
const settlementList = ref<FreightSettlement[]>([])
const selectedRows = ref<FreightSettlement[]>([])
const viewingRow = ref<FreightSettlement | null>(null)
const viewDialogRef = ref<InstanceType<typeof PlFreightSettlementViewDialog>>()
const approvalDialogRef = ref<InstanceType<typeof PlFreightApprovalDialog>>()
const quickSearchKeyword = ref('')
const searchForm = reactive({ q: '' })
@@ -174,7 +244,7 @@ async function fetchData() {
try {
const res = await freightApi.list({
tab: 'approval_pending',
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
@@ -187,12 +257,25 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleReset() {
searchForm.q = ''
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -221,6 +304,18 @@ function handleReject(row: FreightSettlement) {
approvalDialogRef.value?.openReject(row.id, row.currentApprovalId)
}
function syncViewingRowFromList() {
if (!viewingRow.value?.id) return
const updated = settlementList.value.find(row => row.id === viewingRow.value!.id)
if (updated) viewingRow.value = updated
}
async function handleMutationSuccess() {
await fetchData()
syncViewingRowFromList()
await viewDialogRef.value?.reload()
}
function handleToolbarApprove() {
if (selectedRows.value.length !== 1) return
handleApprove(selectedRows.value[0])
@@ -30,6 +30,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="结算单号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
class="btn-main"
type="primary"
@@ -38,7 +44,15 @@
>
<el-icon><Money /></el-icon>发放登记
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -67,17 +81,47 @@
<el-link type="primary" @click="handleView(row)">{{ row.settlementNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="totalFreight" label="运费合计(元)" min-width="130" align="right">
<el-table-column
v-if="isColumnVisible('totalFreight')"
prop="totalFreight"
label="运费合计(元)"
min-width="130"
align="right"
>
<template #default="{ row }">{{ formatAmount(row.totalFreight) }}</template>
</el-table-column>
<el-table-column prop="planCount" label="关联计划数" min-width="110" align="center" />
<el-table-column prop="licensePlateSummary" label="车牌摘要" min-width="140" show-overflow-tooltip />
<el-table-column prop="approvalFinishedTime" label="审批完成时间" min-width="160" align="center">
<el-table-column
v-if="isColumnVisible('planCount')"
prop="planCount"
label="关联计划数"
min-width="110"
align="center"
/>
<el-table-column
v-if="isColumnVisible('licensePlateSummary')"
prop="licensePlateSummary"
label="车牌摘要"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('approvalFinishedTime')"
prop="approvalFinishedTime"
label="审批完成时间"
min-width="160"
align="center"
>
<template #default="{ row }">{{
row.approvalFinishedTime ? formatDateTime(row.approvalFinishedTime) : '—'
}}</template>
</el-table-column>
<el-table-column prop="submitterName" label="提交人" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('submitterName')"
prop="submitterName"
label="提交人"
min-width="100"
align="center"
>
<template #default="{ row }">{{ row.submitterName || '—' }}</template>
</el-table-column>
</el-table>
@@ -107,7 +151,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { Money, RefreshLeft, Search } from '@element-plus/icons-vue'
import { freightApi, type FreightSettlement } from '@/api/pl'
import PlFreightDisburseDialog from '@/components/business/pl-freight-disburse-dialog/index.vue'
@@ -115,6 +161,22 @@ import PlFreightSettlementViewDialog from '@/components/business/pl-freight-sett
import { formatMoneyDisplay } from '@/utils/money-format'
import { formatDateTime } from '@/utils/format'
const listColumnDefs = [
{ prop: 'totalFreight', label: '运费合计(元)' },
{ prop: 'planCount', label: '关联计划数' },
{ prop: 'licensePlateSummary', label: '车牌摘要' },
{ prop: 'approvalFinishedTime', label: '审批完成时间' },
{ prop: 'submitterName', label: '提交人' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-freight-disburse-columns', listColumnDefs)
const emit = defineEmits<{ disbursed: [] }>()
const loading = ref(false)
@@ -123,6 +185,7 @@ const selectedRows = ref<FreightSettlement[]>([])
const viewingRow = ref<FreightSettlement | null>(null)
const viewDialogRef = ref<InstanceType<typeof PlFreightSettlementViewDialog>>()
const disburseDialogRef = ref<InstanceType<typeof PlFreightDisburseDialog>>()
const quickSearchKeyword = ref('')
const searchForm = reactive({ q: '' })
@@ -146,7 +209,7 @@ async function fetchData() {
try {
const res = await freightApi.list({
tab: 'disburse',
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
@@ -159,12 +222,25 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleReset() {
searchForm.q = ''
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -192,8 +268,16 @@ function handleToolbarDisburse() {
handleDisburse(selectedRows.value[0])
}
function handleDisburseSuccess() {
fetchData()
function syncViewingRowFromList() {
if (!viewingRow.value?.id) return
const updated = settlementList.value.find(row => row.id === viewingRow.value!.id)
if (updated) viewingRow.value = updated
}
async function handleDisburseSuccess() {
await fetchData()
syncViewingRowFromList()
await viewDialogRef.value?.reload()
emit('disbursed')
}
@@ -27,14 +27,37 @@
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">已选 <span class="sel-num">0</span> 项</div>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<div class="list-toolbar-selected">已选 <span class="sel-num">{{ selectedRows.length }}</span> 项</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="结算单号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<el-table class="list-table" :data="settlementList" v-loading="loading" stripe row-key="id">
<el-table
class="list-table"
:data="settlementList"
v-loading="loading"
stripe
row-key="id"
@row-dblclick="handleView"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
@@ -48,20 +71,44 @@
<el-link type="primary" @click="handleView(row)">{{ row.settlementNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="totalFreight" label="运费合计(元)" min-width="130" align="right">
<el-table-column
v-if="isColumnVisible('totalFreight')"
prop="totalFreight"
label="运费合计(元)"
min-width="130"
align="right"
>
<template #default="{ row }">{{ formatAmount(row.totalFreight) }}</template>
</el-table-column>
<el-table-column prop="disburseAmount" label="发放金额(元)" min-width="130" align="right">
<el-table-column
v-if="isColumnVisible('disburseAmount')"
prop="disburseAmount"
label="发放金额(元)"
min-width="130"
align="right"
>
<template #default="{ row }">{{
row.disburseAmount ? formatAmount(row.disburseAmount) : '—'
}}</template>
</el-table-column>
<el-table-column prop="disburseTime" label="发放时间" min-width="160" align="center">
<el-table-column
v-if="isColumnVisible('disburseTime')"
prop="disburseTime"
label="发放时间"
min-width="160"
align="center"
>
<template #default="{ row }">{{
row.disburseTime ? formatDateTime(row.disburseTime) : '—'
}}</template>
</el-table-column>
<el-table-column prop="settlementStatus" label="结算状态" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('settlementStatus')"
prop="settlementStatus"
label="结算状态"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getSettlementStatusTagType(row.settlementStatus)" size="small">
{{ getSettlementStatusLabel(row.settlementStatus) }}
@@ -88,7 +135,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { RefreshLeft, Search } from '@element-plus/icons-vue'
import { freightApi, type FreightSettlement } from '@/api/pl'
import PlFreightSettlementViewDialog from '@/components/business/pl-freight-settlement-view-dialog/index.vue'
@@ -99,9 +148,26 @@ import {
import { formatMoneyDisplay } from '@/utils/money-format'
import { formatDateTime } from '@/utils/format'
const listColumnDefs = [
{ prop: 'totalFreight', label: '运费合计(元)' },
{ prop: 'disburseAmount', label: '发放金额(元)' },
{ prop: 'disburseTime', label: '发放时间' },
{ prop: 'settlementStatus', label: '结算状态' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-freight-settled-columns', listColumnDefs)
const loading = ref(false)
const selectedRows = ref<FreightSettlement[]>([])
const settlementList = ref<FreightSettlement[]>([])
const viewDialogRef = ref<InstanceType<typeof PlFreightSettlementViewDialog>>()
const quickSearchKeyword = ref('')
const searchForm = reactive({ q: '' })
@@ -111,6 +177,10 @@ const pagination = reactive({
total: 0
})
function handleSelectionChange(rows: FreightSettlement[]) {
selectedRows.value = rows
}
function formatAmount(amount: number) {
if (!amount) return '¥0.00'
return formatMoneyDisplay(amount)
@@ -121,7 +191,7 @@ async function fetchData() {
try {
const res = await freightApi.list({
tab: 'settled',
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
@@ -134,12 +204,25 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleReset() {
searchForm.q = ''
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -50,6 +50,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="结算单号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
v-if="selectedRows.length === 1 && isEditableDraftSettlement(selectedRows[0].settlementStatus)"
class="btn-main"
@@ -100,7 +106,15 @@
>
<el-icon><RefreshLeft /></el-icon>撤回
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -129,35 +143,89 @@
<el-link type="primary" @click="handleView(row)">{{ row.settlementNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="totalFreight" label="运费合计(元)" min-width="130" align="right">
<el-table-column
v-if="isColumnVisible('totalFreight')"
prop="totalFreight"
label="运费合计(元)"
min-width="130"
align="right"
>
<template #default="{ row }">{{ formatAmount(row.totalFreight) }}</template>
</el-table-column>
<el-table-column prop="planCount" label="关联计划数" min-width="110" align="center" />
<el-table-column prop="licensePlateSummary" label="车牌摘要" min-width="140" show-overflow-tooltip />
<el-table-column prop="settlementStatus" label="结算状态" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('planCount')"
prop="planCount"
label="关联计划数"
min-width="110"
align="center"
/>
<el-table-column
v-if="isColumnVisible('licensePlateSummary')"
prop="licensePlateSummary"
label="车牌摘要"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('settlementStatus')"
prop="settlementStatus"
label="结算状态"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getSettlementStatusTagType(row.settlementStatus)" size="small">
{{ getSettlementStatusLabel(row.settlementStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="disburseAmount" label="发放金额(元)" min-width="130" align="right">
<el-table-column
v-if="isColumnVisible('disburseAmount')"
prop="disburseAmount"
label="发放金额(元)"
min-width="130"
align="right"
>
<template #default="{ row }">{{
row.disburseAmount ? formatAmount(row.disburseAmount) : '—'
}}</template>
</el-table-column>
<el-table-column prop="disburseTime" label="发放时间" min-width="160" align="center">
<el-table-column
v-if="isColumnVisible('disburseTime')"
prop="disburseTime"
label="发放时间"
min-width="160"
align="center"
>
<template #default="{ row }">{{
row.disburseTime ? formatDateTime(row.disburseTime) : '—'
}}</template>
</el-table-column>
<el-table-column prop="currentApproverName" label="当前审批人" min-width="110" align="center">
<el-table-column
v-if="isColumnVisible('currentApproverName')"
prop="currentApproverName"
label="当前审批人"
min-width="110"
align="center"
>
<template #default="{ row }">{{ row.currentApproverName || '—' }}</template>
</el-table-column>
<el-table-column prop="submitterName" label="创建人" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('submitterName')"
prop="submitterName"
label="创建人"
min-width="100"
align="center"
>
<template #default="{ row }">{{ row.submitterName || '—' }}</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="160" align="center">
<el-table-column
v-if="isColumnVisible('createTime')"
prop="createTime"
label="创建时间"
min-width="160"
align="center"
>
<template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
</el-table-column>
</el-table>
@@ -229,7 +297,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import {
CircleClose,
Edit,
@@ -251,6 +321,26 @@ import {
import { formatMoneyDisplay } from '@/utils/money-format'
import { formatDateTime } from '@/utils/format'
const listColumnDefs = [
{ prop: 'totalFreight', label: '运费合计(元)' },
{ prop: 'planCount', label: '关联计划数' },
{ prop: 'licensePlateSummary', label: '车牌摘要' },
{ prop: 'settlementStatus', label: '结算状态' },
{ prop: 'disburseAmount', label: '发放金额(元)' },
{ prop: 'disburseTime', label: '发放时间' },
{ prop: 'currentApproverName', label: '当前审批人' },
{ prop: 'submitterName', label: '创建人' },
{ prop: 'createTime', label: '创建时间' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-freight-settlement-bill-columns', listColumnDefs)
const loading = ref(false)
const cancelLoadingMap = reactive<Record<string, boolean>>({})
const carrierList = ref<Carrier[]>([])
@@ -260,6 +350,7 @@ const viewingRow = ref<FreightSettlement | null>(null)
const viewDialogRef = ref<InstanceType<typeof PlFreightSettlementViewDialog>>()
const formDialogRef = ref<InstanceType<typeof PlFreightSettlementFormDialog>>()
const approvalDialogRef = ref<InstanceType<typeof PlFreightApprovalDialog>>()
const quickSearchKeyword = ref('')
const searchForm = reactive({
carrierId: '',
@@ -288,7 +379,7 @@ async function fetchData() {
const res = await freightApi.list({
tab: 'bills',
carrierId: searchForm.carrierId || undefined,
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
settlementStatus: searchForm.settlementStatus || undefined,
page: pagination.page,
pageSize: pagination.pageSize
@@ -302,6 +393,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -310,6 +402,18 @@ function handleReset() {
searchForm.carrierId = ''
searchForm.q = ''
searchForm.settlementStatus = ''
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -50,6 +50,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="计划编号/客户/车牌"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
class="btn-main"
type="primary"
@@ -59,7 +65,15 @@
>
<el-icon><DocumentAdd /></el-icon>生成结算单
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -71,6 +85,7 @@
stripe
row-key="id"
@selection-change="handleSelectionChange"
@row-dblclick="handleViewPlan"
>
<el-table-column type="selection" min-width="48" align="center" />
<el-table-column
@@ -86,25 +101,67 @@
<el-link type="primary" @click="handleViewPlan(row)">{{ row.planNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="planDate" label="计划日期" min-width="120" align="center" />
<el-table-column prop="planType" label="单据类型" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('planDate')"
prop="planDate"
label="计划日期"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('planType')"
prop="planType"
label="单据类型"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getPlanTypeTagType(row.planType)" size="small">
{{ getPlanTypeLabel(row.planType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="客户/供应商" min-width="140" show-overflow-tooltip>
<el-table-column
v-if="isColumnVisible('partnerName')"
prop="partnerName"
label="客户/供应商"
min-width="140"
show-overflow-tooltip
>
<template #default="{ row }">
{{ row.customerName || row.supplierName || '—' }}
</template>
</el-table-column>
<el-table-column prop="licensePlate" label="车牌号" min-width="110" align="center" />
<el-table-column prop="transportCompany" label="运输公司" min-width="140" show-overflow-tooltip />
<el-table-column prop="freightAmount" label="运费(元)" min-width="120" align="right">
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="110"
align="center"
/>
<el-table-column
v-if="isColumnVisible('transportCompany')"
prop="transportCompany"
label="运输公司"
min-width="140"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('freightAmount')"
prop="freightAmount"
label="运费(元)"
min-width="120"
align="right"
>
<template #default="{ row }">{{ formatAmount(row.freightAmount) }}</template>
</el-table-column>
<el-table-column prop="archiveTime" label="归档时间" min-width="160" align="center">
<el-table-column
v-if="isColumnVisible('archiveTime')"
prop="archiveTime"
label="归档时间"
min-width="160"
align="center"
>
<template #default="{ row }">{{
row.archiveTime ? formatDateTime(row.archiveTime) : '—'
}}</template>
@@ -129,7 +186,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { DocumentAdd, RefreshLeft, Search } from '@element-plus/icons-vue'
import { carrierApi, freightApi, type Carrier, type UnsettledPlanItem } from '@/api/pl'
import { pickBxList } from '@/utils/http/bx'
@@ -140,6 +199,24 @@ import { formatDateTime } from '@/utils/format'
const emit = defineEmits<{ created: [settlementId: string] }>()
const listColumnDefs = [
{ prop: 'planDate', label: '计划日期' },
{ prop: 'planType', label: '单据类型' },
{ prop: 'partnerName', label: '客户/供应商' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'transportCompany', label: '运输公司' },
{ prop: 'freightAmount', label: '运费(元)' },
{ prop: 'archiveTime', label: '归档时间' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-freight-unsettled-plan-columns', listColumnDefs)
const loading = ref(false)
const createLoading = ref(false)
const carrierList = ref<Carrier[]>([])
@@ -147,6 +224,7 @@ const planList = ref<UnsettledPlanItem[]>([])
const selectedRows = ref<UnsettledPlanItem[]>([])
const planViewVisible = ref(false)
const viewPlanId = ref('')
const quickSearchKeyword = ref('')
const searchForm = reactive({
carrierId: '',
@@ -171,7 +249,7 @@ async function fetchData() {
try {
const res = await freightApi.listUnsettledPlans({
carrierId: searchForm.carrierId || undefined,
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
planType: searchForm.planType || undefined,
licensePlate: searchForm.licensePlate || undefined,
page: pagination.page,
@@ -186,6 +264,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -195,6 +274,18 @@ function handleReset() {
searchForm.q = ''
searchForm.planType = ''
searchForm.licensePlate = ''
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
+1 -1
View File
@@ -8,7 +8,7 @@
<el-tabs
v-model="activeTab"
class="panel-pill-tabs panel-pill-tabs--lg"
class="panel-tabs"
@tab-change="handleTabChange"
>
<el-tab-pane label="未结算计划" name="unsettled">
@@ -34,6 +34,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="主体名称/证件号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
class="btn-main"
type="primary"
@@ -44,14 +50,22 @@
<el-icon><CircleCheck /></el-icon>处理
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="reminderList"
:data="displayReminderList"
v-loading="loading"
stripe
row-key="id"
@@ -59,7 +73,13 @@
>
<el-table-column type="selection" min-width="55" align="center" />
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
<el-table-column prop="ownerType" label="主体类型" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('ownerType')"
prop="ownerType"
label="主体类型"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getOwnerTypeTagType(row.ownerType)" size="small">
{{ getOwnerTypeLabel(row.ownerType) }}
@@ -71,30 +91,66 @@
{{ row.ownerLabel || row.driverName || '-' }}
</template>
</el-table-column>
<el-table-column prop="documentType" label="证件类型" min-width="120" align="center">
<el-table-column
v-if="isColumnVisible('documentType')"
prop="documentType"
label="证件类型"
min-width="120"
align="center"
>
<template #default="{ row }">
<el-tag :type="getDocTypeTagType(row.documentType)" size="small">
{{ getDocTypeLabel(row.documentType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="documentNo" label="证件号" min-width="160" align="center" />
<el-table-column prop="expireDate" label="到期日期" min-width="120" align="center" />
<el-table-column prop="daysRemaining" label="剩余天数" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('documentNo')"
prop="documentNo"
label="证件号"
min-width="160"
align="center"
/>
<el-table-column
v-if="isColumnVisible('expireDate')"
prop="expireDate"
label="到期日期"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('daysRemaining')"
prop="daysRemaining"
label="剩余天数"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="getDaysTagType(row.daysRemaining)" size="small">
{{ row.daysRemaining }} 天
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('status')"
prop="status"
label="状态"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag :type="row.status === 'resolved' ? 'success' : 'warning'" size="small">
{{ row.status === 'resolved' ? '已处理' : '待处理' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="处理备注" min-width="160" show-overflow-tooltip />
<el-table-column
v-if="isColumnVisible('remark')"
prop="remark"
label="处理备注"
min-width="160"
show-overflow-tooltip
/>
</el-table>
</div>
</section>
@@ -102,20 +158,58 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { CircleCheck, RefreshLeft, Search } from '@element-plus/icons-vue'
import { inventoryApi, type DocumentReminder } from '@/api/pl'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
const listColumnDefs = [
{ prop: 'ownerType', label: '主体类型' },
{ prop: 'documentType', label: '证件类型' },
{ prop: 'documentNo', label: '证件号' },
{ prop: 'expireDate', label: '到期日期' },
{ prop: 'daysRemaining', label: '剩余天数' },
{ prop: 'status', label: '状态' },
{ prop: 'remark', label: '处理备注', defaultVisible: false }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-inventory-document-reminder-list-columns', listColumnDefs)
const loading = ref(false)
const resolveLoading = ref(false)
const reminderList = ref<DocumentReminder[]>([])
const reminderSourceList = ref<DocumentReminder[]>([])
const quickSearchKeyword = ref('')
const selectedRows = ref<DocumentReminder[]>([])
const searchForm = reactive({
status: ''
})
const displayReminderList = computed(() => {
const keyword = quickSearchKeyword.value.trim().toLowerCase()
if (!keyword) return reminderSourceList.value
return reminderSourceList.value.filter((row) => {
const haystack = [
row.ownerLabel,
row.driverName,
row.documentNo,
getDocTypeLabel(row.documentType)
]
.filter(Boolean)
.join(' ')
.toLowerCase()
return haystack.includes(keyword)
})
})
const selectedRow = computed(() =>
selectedRows.value.length === 1 ? selectedRows.value[0] : null
)
@@ -164,21 +258,31 @@ async function fetchData() {
loading.value = true
try {
const res = await inventoryApi.documentReminders(searchForm.status || undefined)
reminderList.value = (res.data ?? res) || []
reminderSourceList.value = (res.data ?? res) || []
} finally {
loading.value = false
}
}
function handleSearch() {
quickSearchKeyword.value = ''
fetchData()
}
function handleReset() {
searchForm.status = ''
quickSearchKeyword.value = ''
fetchData()
}
function handleQuickSearch() {
// 前端过滤,无需重新请求
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
}
function handleSelectionChange(rows: DocumentReminder[]) {
selectedRows.value = rows
}
+99 -11
View File
@@ -33,9 +33,24 @@
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">已选 <span class="sel-num">0</span> 项</div>
<div class="list-toolbar-selected">已选 <span class="sel-num">{{ selectedRows.length }}</span> 项</div>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="产品名称/编号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -46,8 +61,10 @@
v-loading="loading"
stripe
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
@@ -55,11 +72,33 @@
header-align="center"
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column prop="productName" label="产品名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="productNo" label="产品编号" min-width="120" />
<el-table-column prop="model" label="规格型号" min-width="120" />
<el-table-column prop="unit" label="单位" min-width="80" align="center" />
<el-table-column prop="stockQty" label="库存数量" min-width="120" align="right">
<el-table-column prop="productName" label="产品名称" min-width="180" show-overflow-tooltip />
<el-table-column
v-if="isColumnVisible('productNo')"
prop="productNo"
label="产品编号"
min-width="120"
/>
<el-table-column
v-if="isColumnVisible('model')"
prop="model"
label="规格型号"
min-width="120"
/>
<el-table-column
v-if="isColumnVisible('unit')"
prop="unit"
label="单位"
min-width="80"
align="center"
/>
<el-table-column
v-if="isColumnVisible('stockQty')"
prop="stockQty"
label="库存数量"
min-width="120"
align="right"
>
<template #default="{ row }">
<span
:class="{
@@ -71,12 +110,22 @@
</span>
</template>
</el-table-column>
<el-table-column label="预警阈值" min-width="150" align="center">
<el-table-column
v-if="isColumnVisible('warnThreshold')"
label="预警阈值"
min-width="150"
align="center"
>
<template #default="{ row }">
{{ row.lowStockWarning || '-' }} ~ {{ row.highStockWarning || '-' }}
</template>
</el-table-column>
<el-table-column label="状态" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('status')"
label="状态"
min-width="100"
align="center"
>
<template #default="{ row }">
<el-tag v-if="row.stockQty < (row.lowStockWarning || 0)" type="danger" size="small">
库存不足
@@ -110,13 +159,34 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { RefreshLeft, Search } from '@element-plus/icons-vue'
import { inventoryApi, type InventoryRecord } from '@/api/pl'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
const listColumnDefs = [
{ prop: 'productNo', label: '产品编号' },
{ prop: 'model', label: '规格型号' },
{ prop: 'unit', label: '单位' },
{ prop: 'stockQty', label: '库存数量' },
{ prop: 'warnThreshold', label: '预警阈值' },
{ prop: 'status', label: '状态' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-inventory-ledger-list-columns', listColumnDefs)
const loading = ref(false)
const selectedRows = ref<any[]>([])
const inventoryList = ref<InventoryRecord[]>([])
const quickSearchKeyword = ref('')
const searchForm = reactive({
productName: ''
@@ -128,11 +198,16 @@ const pagination = reactive({
total: 0
})
function handleSelectionChange(rows: any[]) {
selectedRows.value = rows
}
async function fetchData() {
loading.value = true
try {
const keyword = quickSearchKeyword.value.trim()
const res = await inventoryApi.ledger({
productName: searchForm.productName || undefined,
productName: keyword || searchForm.productName || undefined,
page: pagination.page,
pageSize: pagination.pageSize
})
@@ -145,12 +220,25 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleReset() {
searchForm.productName = ''
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
+26 -10
View File
@@ -1,4 +1,4 @@
<!-- 人员管理 -->
<!-- 驾押人员管理 -->
<template>
<div class="page-container">
<section class="panel">
@@ -44,6 +44,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="姓名/手机号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增人员
</el-button>
@@ -193,12 +199,12 @@
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="1120px"
width="1320px"
align-center
class="dialog-form-layout"
@close="handleDialogClose"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="right" label-width="100px" class="pl-dialog-form">
<div class="form-grid-3">
<el-form-item label="姓名" prop="personnelName">
<el-input v-model="formData.personnelName" placeholder="请输入姓名" maxlength="100" />
@@ -235,13 +241,14 @@
</el-form-item>
</div>
<!-- 人员证书(卡片) -->
<!-- 人员证书(列表) -->
<PlCertFormList
ref="certFormListRef"
v-model="certList"
title="人员证书"
upload-folder="pl/personnel-cert"
:cert-name-options="certNameOptions"
layout="table"
/>
</el-form>
<template #footer>
@@ -258,7 +265,7 @@
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewDetail?.personnelName || '人员详情'"
:title="viewDialogTitle"
:icon="User"
:meta="viewHeroMeta"
:sections="viewSections"
@@ -282,7 +289,7 @@
</el-button>
</template>
<template #certs>
<PlCertViewList :cert-list="viewDetail?.certList || []" />
<PlCertViewList :cert-list="viewDetail?.certList || []" layout="table" />
</template>
</SystemViewDialog>
</div>
@@ -297,12 +304,14 @@ import SystemViewDialog, {
type ViewSection
} from '@/components/business/system-view-dialog/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { pickBxPageResult } from '@/utils/http/bx'
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
import { formatDateTime } from '@/utils/format'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import { getPersonnelCertMissingSuffix } from '@/utils/pl-personnel-cert'
type CertListItem = CertRow & { _key?: string }
type CertFormListRef = InstanceType<typeof PlCertFormList>
@@ -340,10 +349,11 @@ const viewLoading = ref(false)
const viewDetail = ref<PersonnelDetail | null>(null)
const certNameOptions = [
'驾驶证',
'押运员证',
'危化品从业资格证',
'身份证',
'驾驶证',
'从业资格证',
'危化品从业资格证',
'押运员证',
'健康证'
]
@@ -397,6 +407,12 @@ const viewHeroMeta = computed(() => {
return parts.join(' · ')
})
const viewDialogTitle = computed(() => {
const name = viewDetail.value?.personnelName || '人员详情'
if (!viewDetail.value) return name
return name + getPersonnelCertMissingSuffix(viewDetail.value.certList || [])
})
const viewSections = computed<ViewSection[]>(() => {
const d = viewDetail.value
if (!d) return []
@@ -427,7 +443,7 @@ const viewSections = computed<ViewSection[]>(() => {
tableMode: 'triple'
},
{
title: '人员证书',
title: `人员证书${viewDetail.value ? getPersonnelCertMissingSuffix(viewDetail.value.certList || []) : ''}`,
fields: [],
slot: 'certs'
}
+55 -74
View File
@@ -29,7 +29,7 @@
<el-option label="草稿" value="draft" />
<el-option label="进行中" value="processing" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
<el-option label="已作废" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item label="运输状态">
@@ -38,52 +38,33 @@
<el-option label="已安排" value="arranged" />
</el-select>
</el-form-item>
</div>
<div v-show="filterExpanded" id="pl-plan-archive-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="客户名称">
<el-input
v-model="searchForm.customerName"
placeholder="搜索客户名称"
clearable
/>
</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>
<el-form-item label="归档状态">
<el-select v-model="searchForm.archiveStatus" placeholder="全部" clearable>
<el-option label="未归档" value="unarchived" />
<el-option label="已归档" value="archived" />
</el-select>
</el-form-item>
</div>
<el-form-item label="客户名称">
<el-input
v-model="searchForm.customerName"
placeholder="搜索客户名称"
clearable
/>
</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>
<el-form-item label="归档状态">
<el-select v-model="searchForm.archiveStatus" placeholder="全部" clearable>
<el-option label="未归档" value="unarchived" />
<el-option label="已归档" value="archived" />
</el-select>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-start">
<button
v-if="showFilterToggle"
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="pl-plan-archive-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -106,18 +87,12 @@
aria-label="计划日期快捷筛选"
@change="handleDateShortcut"
/>
<el-input
<ListQuickSearchInput
v-model="quickSearch"
class="quick-search-input"
placeholder="计划编号/客户名称"
clearable
@keyup.enter="handleQuickSearch"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
>
<template #suffix>
<el-icon class="quick-search-icon" @click="handleQuickSearch"><Search /></el-icon>
</template>
</el-input>
/>
<el-button
v-if="selectedPlan && selectedPlan.followStatus !== 'cancelled'"
class="btn-ghost"
@@ -283,10 +258,14 @@
<PlPlanFormDialog
v-model:visible="formDialogVisible"
:plan-id="editingPlanId"
@success="fetchData"
@success="handlePlanMutationSuccess"
/>
<PlPlanViewDialog v-model:visible="viewVisible" :plan-id="viewPlanId">
<PlPlanViewDialog
ref="viewDialogRef"
v-model:visible="viewVisible"
:plan-id="viewPlanId"
>
<template v-if="viewingPlan" #hero-actions>
<el-button
v-if="viewingPlan.followStatus !== 'cancelled'"
@@ -309,17 +288,16 @@
<PlPlanArchiveDialog
v-model:visible="archiveDialogVisible"
:plan-id="archivePlanId"
@success="fetchData"
@success="handlePlanMutationSuccess"
/>
</div>
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import {
ArrowDownBold,
ArrowUpBold,
Edit,
FolderChecked,
RefreshLeft,
@@ -337,15 +315,16 @@ import {
getPlPlanArchiveStatusLabel,
getPlPlanArchiveStatusTagType
} from '@/utils/pl-display'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
const loading = ref(false)
const formDialogVisible = ref(false)
const editingPlanId = ref('')
const viewVisible = ref(false)
const viewPlanId = ref('')
const viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
const archiveDialogVisible = ref(false)
const archivePlanId = ref('')
const filterExpanded = ref(false)
const planList = ref<Plan[]>([])
const listColumnDefs = [
@@ -371,6 +350,21 @@ const {
const selectedRows = ref<Plan[]>([])
const viewingPlan = ref<Plan | null>(null)
function syncViewingPlanFromList() {
syncViewRowFromFlatList(viewingPlan, planList.value)
}
async function refreshViewIfOpen() {
if (!viewVisible.value || !viewPlanId.value) return
syncViewingPlanFromList()
await viewDialogRef.value?.reload()
}
async function handlePlanMutationSuccess() {
await fetchData()
await refreshViewIfOpen()
}
const selectedPlan = computed(() => (selectedRows.value.length === 1 ? selectedRows.value[0] : null))
const dateRange = ref<string[]>([])
const quickSearch = ref('')
@@ -390,9 +384,6 @@ const pagination = reactive({
total: 0
})
const PLAN_FILTER_FIELD_COUNT = 7
const showFilterToggle = computed(() => PLAN_FILTER_FIELD_COUNT > 4)
function getPlanTypeTagType(type: string) {
const map: Record<string, string> = { direct: 'primary', inbound: 'success', outbound: 'warning' }
return map[type] || 'info'
@@ -418,7 +409,7 @@ function getFollowStatusLabel(status: string) {
draft: '草稿',
processing: '进行中',
completed: '已完成',
cancelled: '已取消'
cancelled: '已作废'
}
return map[status] || status
}
@@ -462,7 +453,6 @@ function handleReset() {
searchForm.archiveStatus = ''
quickSearch.value = ''
resetDateShortcut()
filterExpanded.value = false
pagination.page = 1
fetchData()
}
@@ -551,13 +541,4 @@ onMounted(() => {
.pl-plan-list-toolbar-row {
flex-wrap: wrap;
}
.quick-search-input {
width: 280px;
flex-shrink: 0;
}
.quick-search-icon {
cursor: pointer;
}
</style>
+179 -85
View File
@@ -30,7 +30,7 @@
<el-option label="草稿" value="draft" />
<el-option label="进行中" value="processing" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
<el-option label="已作废" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item label="运输状态">
@@ -39,46 +39,27 @@
<el-option label="已安排" value="arranged" />
</el-select>
</el-form-item>
</div>
<div v-show="filterExpanded" id="pl-plan-cert-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="客户名称">
<el-input
v-model="searchForm.customerName"
placeholder="搜索客户名称"
clearable
/>
</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>
<el-form-item label="客户名称">
<el-input
v-model="searchForm.customerName"
placeholder="搜索客户名称"
clearable
/>
</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-start">
<button
v-if="showFilterToggle"
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="pl-plan-cert-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -101,27 +82,40 @@
aria-label="计划日期快捷筛选"
@change="handleDateShortcut"
/>
<el-input
<ListQuickSearchInput
v-model="quickSearch"
class="quick-search-input"
placeholder="计划编号/客户名称"
clearable
@keyup.enter="handleQuickSearch"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
>
<template #suffix>
<el-icon class="quick-search-icon" @click="handleQuickSearch"><Search /></el-icon>
</template>
</el-input>
/>
<el-button
v-if="selectedPlan && canUploadCert(selectedPlan)"
v-if="selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan)"
class="btn-main"
type="primary"
:disabled="selectedRows.length !== 1"
:loading="certReadyLoading"
@click="handleToolbarMarkCertReady"
>
<el-icon><CircleCheck /></el-icon>证书齐备
</el-button>
<el-button
v-if="selectedPlan && canUploadCert(selectedPlan)"
:class="selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan) ? 'btn-ghost' : 'btn-main'"
:type="selectedPlan && canManageCertReady(selectedPlan) && !isHazardCertReady(selectedPlan) ? undefined : 'primary'"
:disabled="selectedRows.length !== 1"
@click="handleToolbarUploadCert"
>
<el-icon><Upload /></el-icon>上传证书
</el-button>
<el-button
v-if="selectedPlan && canManageCertReady(selectedPlan) && isHazardCertReady(selectedPlan)"
class="btn-ghost"
:disabled="selectedRows.length !== 1"
:loading="certReadyLoading"
@click="handleToolbarUnmarkCertReady"
>
<el-icon><CircleClose /></el-icon>取消齐备
</el-button>
<ListToolbarEndActions
:loading="loading"
@@ -167,6 +161,23 @@
min-width="120"
align="center"
/>
<el-table-column v-if="isColumnVisible('planAttachments')" label="计划附件" min-width="240">
<template #default="{ row }">
<PlPlanListCertCell :products="row.products" short-hazard-tag />
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('products')" label="产品信息" min-width="240">
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('customerName')"
prop="customerName"
label="客户名称"
min-width="180"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('planType')"
prop="planType"
@@ -181,20 +192,15 @@
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('customerName')"
prop="customerName"
label="客户名称"
min-width="180"
show-overflow-tooltip
/>
<el-table-column v-if="isColumnVisible('products')" label="产品信息" min-width="240">
v-if="isColumnVisible('hazardCertReady')"
label="证书齐备"
min-width="100"
align="center"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" />
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('planAttachments')" label="计划附件" min-width="400">
<template #default="{ row }">
<PlPlanListCertCell :products="row.products" />
<el-tag :type="getHazardCertReadyTagType(row)" size="small">
{{ getHazardCertReadyLabel(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
@@ -261,31 +267,51 @@
</div>
</section>
<PlPlanViewDialog v-model:visible="viewVisible" :plan-id="viewPlanId">
<PlPlanViewDialog
ref="viewDialogRef"
v-model:visible="viewVisible"
:plan-id="viewPlanId"
>
<template v-if="viewingPlan" #hero-actions>
<el-button
v-if="canUploadCert(viewingPlan)"
v-if="canManageCertReady(viewingPlan) && !isHazardCertReady(viewingPlan)"
class="btn-main"
type="primary"
:loading="certReadyLoading"
@click="handleMarkCertReady(viewingPlan)"
>
<el-icon><CircleCheck /></el-icon>证书齐备
</el-button>
<el-button
v-if="canUploadCert(viewingPlan)"
class="btn-ghost"
@click="handleUploadCert(viewingPlan)"
>
<el-icon><Upload /></el-icon>上传证书
</el-button>
<el-button
v-if="canManageCertReady(viewingPlan) && isHazardCertReady(viewingPlan)"
class="btn-ghost"
:loading="certReadyLoading"
@click="handleUnmarkCertReady(viewingPlan)"
>
<el-icon><CircleClose /></el-icon>取消齐备
</el-button>
</template>
</PlPlanViewDialog>
<PlPlanCertUploadDialog
v-model:visible="certDialogVisible"
:plan-id="certPlanId"
@success="fetchData"
@success="handleCertUploadSuccess"
/>
</div>
</template>
<script setup lang="ts">
import {
ArrowDownBold,
ArrowUpBold,
CircleCheck,
CircleClose,
RefreshLeft,
Search,
Upload
@@ -294,27 +320,35 @@ import { planApi, type Plan } from '@/api/pl'
import PlPlanCertUploadDialog from '@/components/business/pl-plan-cert-upload-dialog/index.vue'
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import {
isHazardCertReady,
planHasHazardProducts
} from '@/composables/usePlToxicProductCertCheck'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { useListDateShortcut } from '@/composables/useListDateShortcut'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
const loading = ref(false)
const certReadyLoading = ref(false)
const viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
const viewVisible = ref(false)
const viewPlanId = ref('')
const certDialogVisible = ref(false)
const certPlanId = ref('')
const filterExpanded = ref(false)
const planList = ref<Plan[]>([])
const listColumnDefs = [
{ prop: 'planDate', label: '计划日期' },
{ prop: 'planType', label: '单据类型' },
{ prop: 'customerName', label: '客户名称' },
{ prop: 'products', label: '产品信息' },
{ prop: 'planAttachments', label: '计划附件' },
{ prop: 'products', label: '产品信息' },
{ prop: 'customerName', label: '客户名称' },
{ prop: 'planType', label: '单据类型' },
{ prop: 'hazardCertReady', label: '证书齐备' },
{ prop: 'followStatus', label: '跟进状态' },
{ prop: 'transportStatus', label: '运输状态' },
{ prop: 'licensePlate', label: '车牌号' },
@@ -350,9 +384,6 @@ const pagination = reactive({
total: 0
})
const PLAN_FILTER_FIELD_COUNT = 6
const showFilterToggle = computed(() => PLAN_FILTER_FIELD_COUNT > 4)
function getPlanTypeTagType(type: string) {
const map: Record<string, string> = { direct: 'primary', inbound: 'success', outbound: 'warning' }
return map[type] || 'info'
@@ -378,7 +409,7 @@ function getFollowStatusLabel(status: string) {
draft: '草稿',
processing: '进行中',
completed: '已完成',
cancelled: '已取消'
cancelled: '已作废'
}
return map[status] || status
}
@@ -420,7 +451,6 @@ function handleReset() {
searchForm.customerName = ''
quickSearch.value = ''
resetDateShortcut()
filterExpanded.value = false
pagination.page = 1
fetchData()
}
@@ -459,6 +489,79 @@ function canUploadCert(row: Plan) {
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
}
function canManageCertReady(row: Plan) {
return canUploadCert(row) && planHasHazardProducts(row.products)
}
function getHazardCertReadyLabel(row: Plan) {
if (!planHasHazardProducts(row.products)) return '无需'
return isHazardCertReady(row) ? '已齐备' : '待确认'
}
function getHazardCertReadyTagType(row: Plan) {
if (!planHasHazardProducts(row.products)) return 'info'
return isHazardCertReady(row) ? 'success' : 'warning'
}
async function handleMarkCertReady(row: Plan) {
await ElMessageBox.confirm(
'确认该计划危化品证书已齐备?确认后可安排运输;之后仍可在本页补充上传附件。',
'证书齐备确认',
{ type: 'warning', confirmButtonText: '确认齐备', cancelButtonText: '取消' }
)
certReadyLoading.value = true
try {
await planApi.setHazardCertReady(row.id, true)
ElMessage.success('已标记证书齐备')
await fetchData()
await refreshViewIfOpen()
} finally {
certReadyLoading.value = false
}
}
async function handleUnmarkCertReady(row: Plan) {
await ElMessageBox.confirm('确认取消该计划的证书齐备状态?取消后将无法安排运输。', '取消齐备', {
type: 'warning',
confirmButtonText: '确认取消',
cancelButtonText: '返回'
})
certReadyLoading.value = true
try {
await planApi.setHazardCertReady(row.id, false)
ElMessage.success('已取消证书齐备')
await fetchData()
await refreshViewIfOpen()
} finally {
certReadyLoading.value = false
}
}
function handleToolbarMarkCertReady() {
if (selectedRows.value.length !== 1) return
handleMarkCertReady(selectedRows.value[0])
}
function handleToolbarUnmarkCertReady() {
if (selectedRows.value.length !== 1) return
handleUnmarkCertReady(selectedRows.value[0])
}
function syncViewingPlanFromList() {
syncViewRowFromFlatList(viewingPlan, planList.value)
}
async function refreshViewIfOpen() {
if (!viewVisible.value || !viewPlanId.value) return
syncViewingPlanFromList()
await viewDialogRef.value?.reload()
}
async function handleCertUploadSuccess() {
await fetchData()
await refreshViewIfOpen()
}
function handleView(row: Plan) {
viewingPlan.value = row
viewPlanId.value = row.id
@@ -498,13 +601,4 @@ onMounted(() => {
.pl-plan-list-toolbar-row {
flex-wrap: wrap;
}
.quick-search-input {
width: 280px;
flex-shrink: 0;
}
.quick-search-icon {
cursor: pointer;
}
</style>
+11 -5
View File
@@ -174,7 +174,11 @@
<el-button @click="handleBack">
<el-icon><Close /></el-icon>返回
</el-button>
<el-button v-if="plan.archiveStatus !== 'archived'" type="primary" @click="handleEdit">
<el-button
v-if="plan.archiveStatus !== 'archived' && plan.followStatus !== 'cancelled'"
type="primary"
@click="handleEdit"
>
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button
@@ -189,7 +193,7 @@
<el-icon><Van /></el-icon>{{ plan.transportStatus === 'arranged' ? '调整运输' : '安排运输' }}
</el-button>
<el-button v-if="plan.followStatus !== 'cancelled'" type="danger" @click="handleCancel">
<el-icon><CircleClose /></el-icon>取消
<el-icon><CircleClose /></el-icon>作废
</el-button>
</div>
</section>
@@ -244,7 +248,7 @@ function getFollowStatusTagType(status: string) {
}
function getFollowStatusLabel(status: string) {
const map: Record<string, string> = { draft: '草稿', processing: '进行中', completed: '已完成', cancelled: '已取消' }
const map: Record<string, string> = { draft: '草稿', processing: '进行中', completed: '已完成', cancelled: '已作废' }
return map[status] || status
}
@@ -283,9 +287,11 @@ function handleArrange() {
}
async function handleCancel() {
await ElMessageBox.confirm('确定取消该计划吗?', '提示', { type: 'warning' })
await ElMessageBox.confirm('确认作废该运输计划吗?作废后将无法恢复。', '作废运输计划', {
type: 'warning'
})
await planApi.cancel(planId.value)
ElMessage.success('取消成功')
ElMessage.success('作废成功')
fetchDetail()
}
+143 -114
View File
@@ -30,7 +30,7 @@
<el-option label="草稿" value="draft" />
<el-option label="进行中" value="processing" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
<el-option label="已作废" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item label="运输状态">
@@ -39,46 +39,27 @@
<el-option label="已安排" value="arranged" />
</el-select>
</el-form-item>
</div>
<div v-show="filterExpanded" id="pl-plan-filter-extra" class="filter-extra">
<div class="filter-grid">
<el-form-item label="客户名称">
<el-input
v-model="searchForm.customerName"
placeholder="搜索客户名称"
clearable
/>
</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>
<el-form-item label="客户名称">
<el-input
v-model="searchForm.customerName"
placeholder="搜索客户名称"
clearable
/>
</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-start">
<button
v-if="showFilterToggle"
type="button"
class="btn-link btn-filter-toggle"
:aria-expanded="filterExpanded"
aria-controls="pl-plan-filter-extra"
@click="filterExpanded = !filterExpanded"
>
{{ filterExpanded ? '收起更多' : '展开更多' }}
<el-icon>
<ArrowUpBold v-if="filterExpanded" />
<ArrowDownBold v-else />
</el-icon>
</button>
</div>
<div class="filter-actions-start" />
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
@@ -102,37 +83,23 @@
aria-label="计划日期快捷筛选"
@change="handleDateShortcut"
/>
<el-input
<ListQuickSearchInput
v-model="quickSearch"
class="quick-search-input"
placeholder="计划编号/客户名称"
clearable
@keyup.enter="handleQuickSearch"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
>
<template #suffix>
<el-icon class="quick-search-icon" @click="handleQuickSearch"><Search /></el-icon>
</template>
</el-input>
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增计划
</el-button>
<el-button
v-if="selectedPlan && selectedPlan.archiveStatus !== 'archived'"
v-if="selectedPlan && canEditPlan(selectedPlan)"
class="btn-ghost"
:disabled="selectedRows.length !== 1"
@click="handleToolbarEdit"
>
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button
v-if="selectedPlan && selectedPlan.archiveStatus !== 'archived' && selectedPlan.followStatus !== 'cancelled'"
class="btn-ghost"
:disabled="selectedRows.length !== 1"
@click="handleToolbarUploadCert"
>
<el-icon><Upload /></el-icon>上传证书
</el-button>
<el-button
v-if="selectedPlan && canArrangeTransport(selectedPlan)"
class="btn-ghost"
@@ -151,6 +118,23 @@
>
<el-icon><CircleClose /></el-icon>作废
</el-button>
<el-button
v-if="selectedPlan && canDeletePlan(selectedPlan)"
class="btn-ghost"
type="danger"
:disabled="
selectedRows.length !== 1 || !canDeleteByCreator(selectedPlan)
"
:title="
selectedRows.length === 1
? getDeletePlanDisabledTitle(selectedPlan)
: ''
"
:loading="deleteLoading"
@click="handleToolbarDelete"
>
<el-icon><Delete /></el-icon>删除
</el-button>
<ListToolbarEndActions
:loading="loading"
@@ -242,14 +226,15 @@
min-width="180"
show-overflow-tooltip
/>
<el-table-column v-if="isColumnVisible('products')" label="产品信息" min-width="240">
<el-table-column
v-if="isColumnVisible('products')"
label="产品信息"
min-width="280"
align="left"
header-align="left"
>
<template #default="{ row }">
<PlPlanListProductCell :products="row.products" />
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('planAttachments')" label="计划附件" min-width="240">
<template #default="{ row }">
<PlPlanListCertCell :products="row.products" />
<PlPlanListProductCell show-cert-status :products="row.products" />
</template>
</el-table-column>
<el-table-column
@@ -293,26 +278,23 @@
<PlPlanFormDialog
v-model:visible="formDialogVisible"
:plan-id="editingPlanId"
@success="fetchData"
@success="handlePlanMutationSuccess"
/>
<PlPlanViewDialog v-model:visible="viewVisible" :plan-id="viewPlanId">
<PlPlanViewDialog
ref="viewDialogRef"
v-model:visible="viewVisible"
:plan-id="viewPlanId"
>
<template v-if="viewingPlan" #hero-actions>
<el-button
v-if="viewingPlan.archiveStatus !== 'archived'"
v-if="canEditPlan(viewingPlan)"
class="btn-main"
type="primary"
@click="handleEdit(viewingPlan)"
>
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button
v-if="viewingPlan.archiveStatus !== 'archived' && viewingPlan.followStatus !== 'cancelled'"
class="btn-ghost"
@click="handleUploadCert(viewingPlan)"
>
<el-icon><Upload /></el-icon>上传证书
</el-button>
<el-button
v-if="canArrangeTransport(viewingPlan)"
class="btn-ghost"
@@ -329,61 +311,64 @@
>
<el-icon><CircleClose /></el-icon>作废
</el-button>
<el-button
v-if="canDeletePlan(viewingPlan)"
class="btn-ghost"
type="danger"
:disabled="!canDeleteByCreator(viewingPlan)"
:title="getDeletePlanDisabledTitle(viewingPlan)"
:loading="deleteLoading"
@click="handleDelete(viewingPlan)"
>
<el-icon><Delete /></el-icon>删除
</el-button>
</template>
</PlPlanViewDialog>
<PlPlanCertUploadDialog
v-model:visible="certDialogVisible"
:plan-id="certPlanId"
@success="fetchData"
/>
<PlPlanArrangeDialog
v-model:visible="arrangeDialogVisible"
:plan-id="arrangePlanId"
@success="fetchData"
@success="handlePlanMutationSuccess"
/>
</div>
</template>
<script setup lang="ts">
import {
ArrowDownBold,
ArrowUpBold,
CircleClose,
Delete,
Edit,
Plus,
RefreshLeft,
Search,
Upload,
Van
} from '@element-plus/icons-vue'
import { planApi, type Plan } from '@/api/pl'
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'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import PlPlanListCertCell from '@/components/business/pl-plan-list-cert-cell/index.vue'
import PlPlanListProductCell from '@/components/business/pl-plan-list-product-cell/index.vue'
import PlPlanViewDialog from '@/components/business/pl-plan-view-dialog/index.vue'
import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortcuts/index.vue'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { useListDateShortcut } from '@/composables/useListDateShortcut'
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const voidLoading = ref(false)
const deleteLoading = ref(false)
const formDialogVisible = ref(false)
const editingPlanId = ref('')
const viewVisible = ref(false)
const viewPlanId = ref('')
const certDialogVisible = ref(false)
const certPlanId = ref('')
const viewDialogRef = ref<InstanceType<typeof PlPlanViewDialog>>()
const arrangeDialogVisible = ref(false)
const arrangePlanId = ref('')
const filterExpanded = ref(false)
const planList = ref<Plan[]>([])
const listColumnDefs = [
@@ -393,7 +378,6 @@ const listColumnDefs = [
{ prop: 'planType', label: '单据类型' },
{ prop: 'customerName', label: '客户名称' },
{ prop: 'products', label: '产品信息' },
{ prop: 'planAttachments', label: '计划附件' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'personnelSummary', label: '人员' },
{ prop: 'followUserName', label: '跟进人', defaultVisible: false }
@@ -409,6 +393,22 @@ const {
const selectedRows = ref<Plan[]>([])
const viewingPlan = ref<Plan | null>(null)
function syncViewingPlanFromList() {
syncViewRowFromFlatList(viewingPlan, planList.value)
}
async function refreshViewIfOpen() {
if (!viewVisible.value || !viewPlanId.value) return
syncViewingPlanFromList()
await viewDialogRef.value?.reload()
}
async function handlePlanMutationSuccess() {
await fetchData()
await refreshViewIfOpen()
}
const selectedPlan = computed(() => (selectedRows.value.length === 1 ? selectedRows.value[0] : null))
const dateRange = ref<string[]>([])
const quickSearch = ref('')
@@ -427,9 +427,6 @@ const pagination = reactive({
total: 0
})
const PLAN_FILTER_FIELD_COUNT = 6
const showFilterToggle = computed(() => PLAN_FILTER_FIELD_COUNT > 4)
function getPlanTypeTagType(type: string) {
const map: Record<string, string> = { direct: 'primary', inbound: 'success', outbound: 'warning' }
return map[type] || 'info'
@@ -455,7 +452,7 @@ function getFollowStatusLabel(status: string) {
draft: '草稿',
processing: '进行中',
completed: '已完成',
cancelled: '已取消'
cancelled: '已作废'
}
return map[status] || status
}
@@ -497,7 +494,6 @@ function handleReset() {
searchForm.customerName = ''
quickSearch.value = ''
resetDateShortcut()
filterExpanded.value = false
pagination.page = 1
fetchData()
}
@@ -548,6 +544,10 @@ function handleEdit(row: Plan) {
formDialogVisible.value = true
}
function canEditPlan(row: Plan) {
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
}
function canArrangeTransport(row: Plan) {
return (
(row.transportStatus === 'pending' || row.transportStatus === 'arranged') &&
@@ -564,6 +564,49 @@ function canVoidPlan(row: Plan) {
return row.archiveStatus !== 'archived' && row.followStatus !== 'cancelled'
}
function canDeletePlan(row: Plan) {
return (
row.followStatus === 'draft' &&
row.transportStatus === 'pending' &&
row.archiveStatus !== 'archived'
)
}
function getDeletePlanDisabledTitle(row: Plan) {
if (!canDeletePlan(row)) return '只能删除草稿且未安排运输的计划'
if (!canDeleteByCreator(row)) return CREATOR_DELETE_TIP
return ''
}
async function handleDelete(row: Plan) {
if (!canDeletePlan(row)) {
ElMessage.warning('只能删除草稿且未安排运输的计划')
return
}
if (!canDeleteByCreator(row)) {
ElMessage.warning(CREATOR_DELETE_TIP)
return
}
await ElMessageBox.confirm('确定删除该运输计划吗?删除后不可恢复。', '删除运输计划', {
type: 'warning'
})
deleteLoading.value = true
try {
await planApi.delete(row.id)
ElMessage.success('删除成功')
if (viewingPlan.value?.id === row.id) {
viewVisible.value = false
viewingPlan.value = null
}
fetchData()
} catch (e) {
const message = e instanceof Error ? e.message : '删除失败'
ElMessage.error(message)
} finally {
deleteLoading.value = false
}
}
async function handleVoid(row: Plan) {
const hint =
row.transportStatus === 'arranged'
@@ -583,11 +626,6 @@ async function handleVoid(row: Plan) {
}
}
function handleUploadCert(row: Plan) {
certPlanId.value = row.id
certDialogVisible.value = true
}
function handleArrange(row: Plan) {
arrangePlanId.value = row.id
arrangeDialogVisible.value = true
@@ -598,11 +636,6 @@ function handleToolbarEdit() {
handleEdit(selectedRows.value[0])
}
function handleToolbarUploadCert() {
if (selectedRows.value.length !== 1) return
handleUploadCert(selectedRows.value[0])
}
function handleToolbarArrange() {
if (selectedRows.value.length !== 1) return
handleArrange(selectedRows.value[0])
@@ -613,6 +646,11 @@ function handleToolbarVoid() {
handleVoid(selectedRows.value[0])
}
function handleToolbarDelete() {
if (selectedRows.value.length !== 1) return
handleDelete(selectedRows.value[0])
}
useDbInstantSearch(
() => ({
planNo: searchForm.planNo,
@@ -645,13 +683,4 @@ function openArrangeFromQuery() {
.pl-plan-list-toolbar-row {
flex-wrap: wrap;
}
.quick-search-input {
width: 280px;
flex-shrink: 0;
}
.quick-search-icon {
cursor: pointer;
}
</style>
+47 -13
View File
@@ -53,6 +53,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="车牌号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增自有车辆
</el-button>
@@ -189,12 +195,12 @@
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="960px"
width="1360px"
align-center
class="dialog-form-layout"
@close="handleDialogClose"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="right" label-width="100px" class="pl-dialog-form">
<div class="form-grid-3">
<el-form-item label="车牌号" prop="licensePlate">
<el-input v-model="formData.licensePlate" placeholder="请输入车牌号" maxlength="20" />
@@ -311,6 +317,7 @@
title="车辆证书"
upload-folder="pl/vehicle-cert"
:cert-name-options="certNameOptions"
layout="table"
/>
</el-form>
<template #footer>
@@ -327,7 +334,7 @@
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewDetail?.licensePlate || '车辆详情'"
:title="viewDialogTitle"
:icon="Van"
:meta="viewHeroMeta"
:sections="viewSections"
@@ -355,7 +362,10 @@
:cert-list="viewDetail?.certList || []"
:license-plate="viewDetail?.licensePlate"
:trailer-no="viewDetail?.trailerNo"
:vehicle-id="viewDetail?.id"
:personnel-list="viewPersonnelList"
check-self-vehicle-certs
cert-layout="table"
/>
</template>
</SystemViewDialog>
@@ -364,6 +374,7 @@
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Van } from '@element-plus/icons-vue'
import {
@@ -395,7 +406,8 @@ import {
import {
PL_VEHICLE_CERT_NAME_OPTIONS,
splitVehicleCertsByPart,
validateVehicleRequiredCerts
getSelfVehicleCertMissingSuffix,
validateVehicleCertNamesNotEmpty
} from '@/utils/pl-vehicle-cert'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
@@ -405,6 +417,7 @@ type CertListItem = CertRow & { _key?: string }
type CertFormListRef = InstanceType<typeof PlVehicleCertPartTabs>
const loading = ref(false)
const quickSearchKeyword = ref('')
const submitLoading = ref(false)
const vehicleList = ref<Vehicle[]>([])
const carrierList = ref<Carrier[]>([])
@@ -491,6 +504,18 @@ const viewHeroMeta = computed(() => {
.join(' · ')
})
const viewDialogTitle = computed(() => {
const plate = viewDetail.value?.licensePlate || '车辆详情'
if (!viewDetail.value) return plate
return (
plate +
getSelfVehicleCertMissingSuffix({
certList: viewDetail.value.certList,
trailerNo: viewDetail.value.trailerNo
})
)
})
const viewSections = computed<ViewSection[]>(() => {
const d = viewDetail.value
if (!d) return []
@@ -658,14 +683,10 @@ function validateSubTables(): boolean {
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)
})
return validateVehicleCertNamesNotEmpty(
[headCerts, ...(hasTrailer ? [trailerCerts] : [])],
message => ElMessage.warning(message)
)
}
async function fetchData() {
@@ -673,7 +694,7 @@ async function fetchData() {
try {
const res = await vehicleApi.list({
vehicleType: VEHICLE_TYPE_SELF,
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
vehicleStatus: searchForm.vehicleStatus || undefined,
carrierId: searchForm.carrierId || undefined,
page: pagination.page,
@@ -690,6 +711,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -698,11 +720,23 @@ function handleReset() {
searchForm.q = ''
searchForm.vehicleStatus = ''
searchForm.carrierId = ''
quickSearchKeyword.value = ''
selectedRows.value = []
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handlePageChange() {
fetchData()
}
@@ -42,6 +42,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="名称/地址"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增地址
</el-button>
@@ -61,6 +67,16 @@
>
<el-icon><Delete /></el-icon>删除
</el-button>
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -88,23 +104,58 @@
<el-link type="primary" @click="handleView(row)">{{ row.addressName }}</el-link>
</template>
</el-table-column>
<el-table-column prop="fullAddress" label="详细地址" min-width="240" show-overflow-tooltip />
<el-table-column label="坐标" min-width="160" show-overflow-tooltip>
<el-table-column
v-if="isColumnVisible('fullAddress')"
prop="fullAddress"
label="详细地址"
min-width="240"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('coordinates')"
label="坐标"
min-width="160"
show-overflow-tooltip
>
<template #default="{ row }">
<span v-if="row.lng != null && row.lat != null">{{ row.lng }}, {{ row.lat }}</span>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column prop="contactName" label="联系人" min-width="100" align="center" />
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" />
<el-table-column prop="status" label="状态" min-width="90" align="center">
<el-table-column
v-if="isColumnVisible('contactName')"
prop="contactName"
label="联系人"
min-width="100"
align="center"
/>
<el-table-column
v-if="isColumnVisible('contactPhone')"
prop="contactPhone"
label="联系电话"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('status')"
prop="status"
label="状态"
min-width="90"
align="center"
>
<template #default="{ row }">
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
{{ row.status === '1' ? '启用' : '停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
<el-table-column
v-if="isColumnVisible('remark')"
prop="remark"
label="备注"
min-width="140"
show-overflow-tooltip
/>
</el-table>
<div class="pagination list-pager-bar">
@@ -137,7 +188,9 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="pl-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
@@ -183,12 +236,14 @@
</el-dialog>
<SystemViewDialog
v-model="viewVisible"
title="地址详情"
v-model:visible="viewVisible"
:title="viewDetail?.addressName || '地址详情'"
:meta="viewHeroMeta"
:loading="viewLoading"
:hero-title="viewDetail?.addressName || '—'"
:hero-meta="viewHeroMeta"
:sections="viewSections"
width="1040px"
modal-class="order-module-dialog-mask"
append-to-body
>
<template #hero-actions>
<el-button @click="handleEditFromView">
@@ -214,7 +269,10 @@ import {
type TransportAddressPayload
} from '@/api/pl/index'
import AmapAddressPicker from '@/components/business/amap-address-picker/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import SystemViewDialog, { type ViewSection } from '@/components/business/system-view-dialog/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { pickBxPageResult } from '@/utils/http/bx'
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
@@ -222,6 +280,24 @@ import { formatDateTime } from '@/utils/format'
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
const loading = ref(false)
const quickSearchKeyword = ref('')
const listColumnDefs = [
{ prop: 'fullAddress', label: '详细地址' },
{ prop: 'coordinates', label: '坐标' },
{ prop: 'contactName', label: '联系人' },
{ prop: 'contactPhone', label: '联系电话' },
{ prop: 'status', label: '状态' },
{ prop: 'remark', label: '备注', defaultVisible: false }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-transport-address-list-columns', listColumnDefs)
const submitLoading = ref(false)
const addressRows = ref<TransportAddress[]>([])
const selectedRows = ref<TransportAddress[]>([])
@@ -275,15 +351,25 @@ const viewSections = computed<ViewSection[]>(() => {
return [
{
title: '基本信息',
fieldLayout: 'table',
tableMode: 'triple',
fields: [
{ label: '地址名称', value: d.addressName },
{ label: '详细地址', value: d.fullAddress, span: 'full' },
{ label: '联系人', value: d.contactName || '—' },
{ label: '联系电话', value: d.contactPhone || '—' },
{
label: '详细地址',
value: d.fullAddress,
long: true,
showMapLink: true,
mapLng: d.lng,
mapLat: d.lat,
mapName: d.addressName || d.fullAddress
},
{
label: '坐标',
value: d.lng != null && d.lat != null ? `${d.lng}, ${d.lat}` : '—'
},
{ label: '联系人', value: d.contactName || '—' },
{ label: '联系电话', value: d.contactPhone || '—' },
{
label: '状态',
value: d.status === '1' ? '启用' : '停用',
@@ -291,7 +377,7 @@ const viewSections = computed<ViewSection[]>(() => {
},
{ label: '创建时间', value: formatDateTime(d.createTime) },
{ label: '更新时间', value: formatDateTime(d.updateTime) },
{ label: '备注', value: d.remark || '—', span: 'full' }
{ label: '备注', value: d.remark || '—', long: true }
]
}
]
@@ -301,7 +387,7 @@ async function fetchData() {
loading.value = true
try {
const res = await transportAddressApi.list({
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
status: searchForm.status || undefined,
page: pagination.page,
pageSize: pagination.pageSize
@@ -317,6 +403,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -324,11 +411,23 @@ function handleSearch() {
function handleReset() {
searchForm.q = ''
searchForm.status = ''
quickSearchKeyword.value = ''
selectedRows.value = []
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handlePageChange() {
fetchData()
}
+27 -3
View File
@@ -23,7 +23,7 @@
<el-option label="待执行" value="pending" />
<el-option label="执行中" value="executing" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
<el-option label="已作废" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item label="审批状态">
@@ -53,6 +53,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="运输编号/车牌/司机"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
class="btn-main"
type="primary"
@@ -240,6 +246,7 @@ import SystemViewDialog, {
type ViewSection
} from '@/components/business/system-view-dialog/index.vue'
import { transportApi, type Transport } from '@/api/pl'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
@@ -284,6 +291,8 @@ const canToolbarQrcode = computed(
() => selectedRow.value?.approvalStatus === 'approved'
)
const quickSearchKeyword = ref('')
const searchForm = reactive({
transportNo: '',
transportStatus: '',
@@ -378,7 +387,7 @@ function getTransportStatusLabel(status: string) {
pending: '待执行',
executing: '执行中',
completed: '已完成',
cancelled: '已取消'
cancelled: '已作废'
}
return map[status] || status
}
@@ -411,8 +420,10 @@ function formatAmount(amount: number) {
async function fetchData() {
loading.value = true
try {
const keyword = quickSearchKeyword.value.trim()
const res = await transportApi.list({
transportNo: searchForm.transportNo || undefined,
q: keyword || undefined,
transportNo: keyword ? undefined : searchForm.transportNo || undefined,
transportStatus: searchForm.transportStatus || undefined,
approvalStatus: searchForm.approvalStatus || undefined,
page: pagination.page,
@@ -427,6 +438,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -435,6 +447,18 @@ function handleReset() {
searchForm.transportNo = ''
searchForm.transportStatus = ''
searchForm.approvalStatus = ''
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -67,6 +67,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="申请单号/车牌"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增申请
</el-button>
@@ -87,7 +93,15 @@
<el-icon><CircleClose /></el-icon>作废
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -115,28 +129,76 @@
<el-link type="primary" @click="handleView(row)">{{ row.applyNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
<el-table-column prop="maintenanceType" label="维保类型" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('maintenanceType')"
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">
<el-table-column
v-if="isColumnVisible('applyDate')"
prop="applyDate"
label="申请日期"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('startDate')"
prop="startDate"
label="开始日期"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('expectedPickupDate')"
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">
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
/>
<el-table-column
v-if="isColumnVisible('status')"
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
v-if="isColumnVisible('createTime')"
prop="createTime"
label="创建时间"
min-width="170"
align="center"
/>
</el-table>
<div class="pagination list-pager-bar">
@@ -166,7 +228,9 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="pl-dialog-form"
scroll-to-error
>
<div class="form-grid-2">
@@ -300,7 +364,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import {
Check,
CircleClose,
@@ -328,9 +394,29 @@ import {
getPlMaintenanceTypeTagType,
PL_MAINTENANCE_TYPE_PRESETS
} from '@/utils/pl-display'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
const userStore = useUserStore()
const listColumnDefs = [
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'maintenanceType', label: '维保类型' },
{ prop: 'applyDate', label: '申请日期' },
{ prop: 'startDate', label: '开始日期' },
{ prop: 'expectedPickupDate', label: '预计取车' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'status', label: '状态' },
{ prop: 'createTime', label: '创建时间', defaultVisible: false }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-vehicle-maintenance-apply-list-columns', listColumnDefs)
const currentUserId = computed(() => {
const info = userStore.info as Record<string, unknown> | undefined
return String(info?.id || info?.userId || '')
@@ -347,6 +433,7 @@ const tableData = ref<VehicleMaintenanceApply[]>([])
const selectedRows = ref<VehicleMaintenanceApply[]>([])
const vehicleOptions = ref<Vehicle[]>([])
const dateRange = ref<string[]>([])
const quickSearchKeyword = ref('')
const searchForm = reactive({
q: '',
@@ -394,6 +481,14 @@ const viewLoading = ref(false)
const viewDetail = ref<VehicleMaintenanceApply | null>(null)
const viewingRecord = ref<VehicleMaintenanceApply | null>(null)
async function refreshViewIfOpen(editedId?: string) {
if (!viewVisible.value || !viewingRecord.value?.id) return
if (editedId && viewingRecord.value.id !== editedId) return
syncViewRowFromFlatList(viewingRecord, tableData.value)
const res = await vehicleMaintenanceApplyApi.get(viewingRecord.value.id)
viewDetail.value = res.data ?? res
}
const viewSections = computed<ViewSection[]>(() => {
const d = viewDetail.value
if (!d) return []
@@ -484,7 +579,7 @@ async function fetchData() {
loading.value = true
try {
const res = await vehicleMaintenanceApplyApi.list({
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
maintenanceType: searchForm.maintenanceType || undefined,
status: searchForm.status || undefined,
applyDateFrom: dateRange.value?.[0],
@@ -501,6 +596,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -510,7 +606,20 @@ function handleReset() {
searchForm.maintenanceType = ''
searchForm.status = ''
dateRange.value = []
handleSearch()
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleSelectionChange(rows: VehicleMaintenanceApply[]) {
@@ -625,7 +734,8 @@ async function handleSubmit() {
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
await fetchData()
if (isEdit.value) await refreshViewIfOpen(formData.id)
} finally {
submitLoading.value = false
}
@@ -78,6 +78,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="申请单号/车牌"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
class="btn-main"
type="primary"
@@ -102,7 +108,15 @@
<el-icon><Plus /></el-icon>无申请补录
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -130,39 +144,105 @@
<el-link type="primary" @click="handleView(row)">{{ row.applyNo }}</el-link>
</template>
</el-table-column>
<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">
<el-table-column
v-if="isColumnVisible('applyDate')"
prop="applyDate"
label="申请日期"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
/>
<el-table-column
v-if="isColumnVisible('applyStatus')"
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">
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="110"
align="center"
/>
<el-table-column
v-if="isColumnVisible('maintenanceType')"
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">
<el-table-column
v-if="isColumnVisible('startDate')"
prop="startDate"
label="开始日期"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('expectedPickupDate')"
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>
<el-table-column
v-if="isColumnVisible('processNo')"
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">
<el-table-column
v-if="isColumnVisible('processDate')"
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">
<el-table-column
v-if="isColumnVisible('amount')"
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">
<el-table-column
v-if="isColumnVisible('handlerName')"
prop="handlerName"
label="处理人"
min-width="100"
align="center"
>
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
</el-table-column>
</el-table>
@@ -190,7 +270,7 @@
destroy-on-close
@closed="resetForm"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" scroll-to-error>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="right" label-width="100px" class="pl-dialog-form" scroll-to-error>
<div v-if="dialogMode === 'process' && currentApplyRow" class="apply-summary-block">
<div class="apply-summary-title">关联申请</div>
<div class="apply-summary-grid">
@@ -328,7 +408,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Tools } from '@element-plus/icons-vue'
import { fetchUserOptions } from '@/api/admin'
import {
@@ -364,6 +446,29 @@ const userOptions = ref<
>([])
const loading = ref(false)
const listColumnDefs = [
{ prop: 'applyDate', label: '申请日期' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'applyStatus', label: '申请状态' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'maintenanceType', label: '维保类型' },
{ prop: 'startDate', label: '开始日期' },
{ prop: 'expectedPickupDate', label: '预计取车' },
{ prop: 'processNo', label: '处理单号' },
{ prop: 'processDate', label: '处理日期' },
{ prop: 'amount', label: '金额(元)' },
{ prop: 'handlerName', label: '处理人' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-vehicle-maintenance-process-list-columns', listColumnDefs)
const submitLoading = ref(false)
const deleteLoadingMap = reactive<Record<string, boolean>>({})
const selectedRows = ref<VehicleMaintenanceProcessBoardItem[]>([])
@@ -371,6 +476,7 @@ const tableData = ref<VehicleMaintenanceProcessBoardItem[]>([])
const vehicleOptions = ref<Vehicle[]>([])
const applyDateRange = ref<string[]>([])
const processDateRange = ref<string[]>([])
const quickSearchKeyword = ref('')
const searchForm = reactive({
q: '',
@@ -541,7 +647,7 @@ async function fetchData() {
loading.value = true
try {
const res = await vehicleMaintenanceProcessApi.processBoard({
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
maintenanceType: searchForm.maintenanceType || undefined,
applyStatus: searchForm.applyStatus || undefined,
applyDateFrom: applyDateRange.value?.[0],
@@ -560,6 +666,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -570,7 +677,20 @@ function handleReset() {
searchForm.applyStatus = ''
applyDateRange.value = []
processDateRange.value = []
handleSearch()
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function resetForm() {
@@ -55,6 +55,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="申请单号/车牌"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增申请
</el-button>
@@ -75,7 +81,15 @@
<el-icon><CircleClose /></el-icon>作废
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -103,27 +117,69 @@
<el-link type="primary" @click="handleView(row)">{{ row.applyNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="licensePlate" label="车牌号" min-width="120" align="center" />
<el-table-column prop="rechargeType" label="充值类型" min-width="100" align="center">
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('rechargeType')"
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">
<el-table-column
v-if="isColumnVisible('applyDate')"
prop="applyDate"
label="申请日期"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('odometerKm')"
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">
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
/>
<el-table-column
v-if="isColumnVisible('status')"
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
v-if="isColumnVisible('createTime')"
prop="createTime"
label="创建时间"
min-width="170"
align="center"
/>
</el-table>
<div class="pagination list-pager-bar">
@@ -153,7 +209,9 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="pl-dialog-form"
scroll-to-error
>
<div class="form-grid-2">
@@ -245,7 +303,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import {
Check,
CircleClose,
@@ -272,9 +332,28 @@ import {
getPlRechargeTypeLabel,
getPlRechargeTypeTagType
} from '@/utils/pl-display'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
const userStore = useUserStore()
const listColumnDefs = [
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'rechargeType', label: '充值类型' },
{ prop: 'applyDate', label: '申请日期' },
{ prop: 'odometerKm', label: '期初公里数' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'status', label: '状态' },
{ prop: 'createTime', label: '创建时间', defaultVisible: false }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-vehicle-recharge-apply-list-columns', listColumnDefs)
const currentUserId = computed(() => {
const info = userStore.info as Record<string, unknown> | undefined
return String(info?.id || info?.userId || '')
@@ -291,6 +370,7 @@ const tableData = ref<VehicleRechargeApply[]>([])
const selectedRows = ref<VehicleRechargeApply[]>([])
const vehicleOptions = ref<Vehicle[]>([])
const dateRange = ref<string[]>([])
const quickSearchKeyword = ref('')
const searchForm = reactive({
q: '',
@@ -334,6 +414,14 @@ const viewLoading = ref(false)
const viewDetail = ref<VehicleRechargeApply | null>(null)
const viewingRecord = ref<VehicleRechargeApply | null>(null)
async function refreshViewIfOpen(editedId?: string) {
if (!viewVisible.value || !viewingRecord.value?.id) return
if (editedId && viewingRecord.value.id !== editedId) return
syncViewRowFromFlatList(viewingRecord, tableData.value)
const res = await vehicleRechargeApplyApi.get(viewingRecord.value.id)
viewDetail.value = res.data ?? res
}
const viewSections = computed<ViewSection[]>(() => {
const d = viewDetail.value
if (!d) return []
@@ -389,7 +477,7 @@ async function fetchData() {
loading.value = true
try {
const res = await vehicleRechargeApplyApi.list({
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
rechargeType: searchForm.rechargeType || undefined,
status: searchForm.status || undefined,
applyDateFrom: dateRange.value?.[0],
@@ -406,6 +494,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -415,7 +504,20 @@ function handleReset() {
searchForm.rechargeType = ''
searchForm.status = ''
dateRange.value = []
handleSearch()
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleSelectionChange(rows: VehicleRechargeApply[]) {
@@ -521,7 +623,8 @@ async function handleSubmit() {
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
await fetchData()
if (isEdit.value) await refreshViewIfOpen(formData.id)
} finally {
submitLoading.value = false
}
@@ -66,6 +66,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="申请单号/车牌"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button @click="handleSupplement">
<el-icon><Plus /></el-icon>无申请补录
</el-button>
@@ -105,7 +111,15 @@
<el-icon><Delete /></el-icon>撤销发放
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -133,46 +147,118 @@
<el-link type="primary" @click="handleView(row)">{{ row.applyNo }}</el-link>
</template>
</el-table-column>
<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">
<el-table-column
v-if="isColumnVisible('applyDate')"
prop="applyDate"
label="申请日期"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
/>
<el-table-column
v-if="isColumnVisible('odometerKm')"
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">
<el-table-column
v-if="isColumnVisible('applyStatus')"
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">
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="110"
align="center"
/>
<el-table-column
v-if="isColumnVisible('rechargeType')"
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>
<el-table-column
v-if="isColumnVisible('rechargeOrderNo')"
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">
<el-table-column
v-if="isColumnVisible('rechargeDate')"
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>
<el-table-column
v-if="isColumnVisible('cardNo')"
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>
<el-table-column
v-if="isColumnVisible('gasStation')"
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">
<el-table-column
v-if="isColumnVisible('amount')"
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">
<el-table-column
v-if="isColumnVisible('recordCreateByName')"
prop="recordCreateByName"
label="登记人"
min-width="100"
align="center"
>
<template #default="{ row }">{{ row.recordCreateByName || '—' }}</template>
</el-table-column>
</el-table>
@@ -200,7 +286,7 @@
destroy-on-close
@closed="resetForm"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" scroll-to-error>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="right" label-width="100px" class="pl-dialog-form" scroll-to-error>
<div v-if="dialogMode === 'disburse' && currentApplyRow" class="apply-summary-block">
<div class="apply-summary-title">关联申请</div>
<div class="apply-summary-grid">
@@ -336,7 +422,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { Check, Close, Delete, Edit, Money, Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
vehicleApi,
@@ -362,6 +450,30 @@ const userStore = useUserStore()
const currentUserName = computed(() => userStore.info?.userName || '—')
const loading = ref(false)
const listColumnDefs = [
{ prop: 'applyDate', label: '申请日期' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'odometerKm', label: '期初公里数' },
{ prop: 'applyStatus', label: '申请状态' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'rechargeType', label: '充值类型' },
{ prop: 'rechargeOrderNo', label: '充值订单号' },
{ prop: 'rechargeDate', label: '充值日期' },
{ prop: 'cardNo', label: '卡号' },
{ prop: 'gasStation', label: '加油站/渠道' },
{ prop: 'amount', label: '金额(元)' },
{ prop: 'recordCreateByName', label: '登记人' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-vehicle-recharge-record-list-columns', listColumnDefs)
const submitLoading = ref(false)
const deleteLoadingMap = reactive<Record<string, boolean>>({})
const tableData = ref<VehicleRechargeDisburseBoardItem[]>([])
@@ -369,6 +481,7 @@ const selectedRows = ref<VehicleRechargeDisburseBoardItem[]>([])
const vehicleOptions = ref<Vehicle[]>([])
const applyDateRange = ref<string[]>([])
const rechargeDateRange = ref<string[]>([])
const quickSearchKeyword = ref('')
const searchForm = reactive({
q: '',
@@ -419,6 +532,13 @@ const formRules = computed(() => ({
const viewVisible = ref(false)
const viewingRecord = ref<VehicleRechargeDisburseBoardItem | null>(null)
function refreshViewIfOpen(editedApplyId?: string) {
if (!viewVisible.value || !viewingRecord.value?.applyId) return
if (editedApplyId && viewingRecord.value.applyId !== editedApplyId) return
const updated = tableData.value.find(row => row.applyId === viewingRecord.value!.applyId)
if (updated) viewingRecord.value = updated
}
const viewSections = computed<ViewSection[]>(() => {
const row = viewingRecord.value
if (!row) return []
@@ -477,7 +597,7 @@ async function fetchData() {
loading.value = true
try {
const res = await vehicleRechargeRecordApi.disburseBoard({
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
rechargeType: searchForm.rechargeType || undefined,
applyStatus: searchForm.applyStatus || undefined,
applyDateFrom: applyDateRange.value?.[0],
@@ -496,6 +616,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -506,7 +627,20 @@ function handleReset() {
searchForm.applyStatus = ''
applyDateRange.value = []
rechargeDateRange.value = []
handleSearch()
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleSelectionChange(rows: VehicleRechargeDisburseBoardItem[]) {
@@ -640,7 +774,9 @@ async function handleSubmit() {
ElMessage.success('发放登记成功')
}
dialogVisible.value = false
fetchData()
await fetchData()
const applyId = formData.applyId || currentApplyRow.value?.applyId
if (applyId) refreshViewIfOpen(applyId)
} finally {
submitLoading.value = false
}
@@ -49,6 +49,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="申请单号/车牌"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增申请
</el-button>
@@ -69,7 +75,15 @@
<el-icon><CircleClose /></el-icon>作废
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -97,25 +111,79 @@
<el-link type="primary" @click="handleView(row)">{{ row.applyNo }}</el-link>
</template>
</el-table-column>
<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">
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('applyDate')"
prop="applyDate"
label="申请时间"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('warehouseInDate')"
prop="warehouseInDate"
label="入库时间"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('unit')"
prop="unit"
label="单位"
min-width="90"
align="center"
/>
<el-table-column
v-if="isColumnVisible('quantity')"
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">
<el-table-column
v-if="isColumnVisible('applyAmount')"
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">
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
/>
<el-table-column
v-if="isColumnVisible('status')"
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
v-if="isColumnVisible('createTime')"
prop="createTime"
label="创建时间"
min-width="170"
align="center"
/>
</el-table>
<div class="pagination list-pager-bar">
@@ -145,7 +213,9 @@
ref="formRef"
:model="formData"
:rules="formRules"
label-position="top"
label-position="right"
label-width="100px"
class="pl-dialog-form"
scroll-to-error
>
<div class="form-grid-2">
@@ -249,7 +319,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import {
Check,
CircleClose,
@@ -275,6 +347,7 @@ import {
getPlUreaApplyStatusTagType,
PL_UREA_UNIT_PRESETS
} from '@/utils/pl-display'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
const userStore = useUserStore()
@@ -288,12 +361,34 @@ const userOptions = ref<
>([])
const loading = ref(false)
const listColumnDefs = [
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'applyDate', label: '申请时间' },
{ prop: 'warehouseInDate', label: '入库时间' },
{ prop: 'unit', label: '单位' },
{ prop: 'quantity', label: '入库数' },
{ prop: 'applyAmount', label: '申请金额(元)' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'status', label: '状态' },
{ prop: 'createTime', label: '创建时间' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-vehicle-urea-apply-list-columns', listColumnDefs)
const submitLoading = ref(false)
const cancelLoadingMap = reactive<Record<string, boolean>>({})
const tableData = ref<VehicleUreaApply[]>([])
const selectedRows = ref<VehicleUreaApply[]>([])
const vehicleOptions = ref<Vehicle[]>([])
const dateRange = ref<string[]>([])
const quickSearchKeyword = ref('')
const searchForm = reactive({
q: '',
@@ -341,6 +436,14 @@ const viewLoading = ref(false)
const viewDetail = ref<VehicleUreaApply | null>(null)
const viewingRecord = ref<VehicleUreaApply | null>(null)
async function refreshViewIfOpen(editedId?: string) {
if (!viewVisible.value || !viewingRecord.value?.id) return
if (editedId && viewingRecord.value.id !== editedId) return
syncViewRowFromFlatList(viewingRecord, tableData.value)
const res = await vehicleUreaApplyApi.get(viewingRecord.value.id)
viewDetail.value = res.data ?? res
}
const viewSections = computed<ViewSection[]>(() => {
const d = viewDetail.value
if (!d) return []
@@ -394,7 +497,7 @@ async function fetchData() {
loading.value = true
try {
const res = await vehicleUreaApplyApi.list({
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
status: searchForm.status || undefined,
applyDateFrom: dateRange.value?.[0],
applyDateTo: dateRange.value?.[1],
@@ -410,6 +513,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -418,7 +522,20 @@ function handleReset() {
searchForm.q = ''
searchForm.status = ''
dateRange.value = []
handleSearch()
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleSelectionChange(rows: VehicleUreaApply[]) {
@@ -536,7 +653,8 @@ async function handleSubmit() {
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
await fetchData()
if (isEdit.value) await refreshViewIfOpen(formData.id)
} finally {
submitLoading.value = false
}
@@ -60,6 +60,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="申请单号/车牌"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button
class="btn-main"
type="primary"
@@ -84,7 +90,15 @@
<el-icon><Plus /></el-icon>无申请补录
</el-button>
<ListToolbarEndActions :loading="loading" @refresh="fetchData" />
<ListToolbarEndActions
:loading="loading"
:column-defs="listColumnDefs"
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="fetchData"
@reset-columns="resetToDefault"
/>
</div>
</div>
@@ -112,36 +126,108 @@
<el-link type="primary" @click="handleView(row)">{{ row.applyNo }}</el-link>
</template>
</el-table-column>
<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">
<el-table-column
v-if="isColumnVisible('applyDate')"
prop="applyDate"
label="申请时间"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('warehouseInDate')"
prop="warehouseInDate"
label="入库时间"
min-width="120"
align="center"
/>
<el-table-column
v-if="isColumnVisible('applicantName')"
prop="applicantName"
label="申请人"
min-width="100"
align="center"
/>
<el-table-column
v-if="isColumnVisible('unit')"
prop="unit"
label="单位"
min-width="90"
align="center"
/>
<el-table-column
v-if="isColumnVisible('quantity')"
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">
<el-table-column
v-if="isColumnVisible('applyAmount')"
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">
<el-table-column
v-if="isColumnVisible('applyStatus')"
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>
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="110"
align="center"
/>
<el-table-column
v-if="isColumnVisible('disburseNo')"
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">
<el-table-column
v-if="isColumnVisible('disburseDate')"
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">
<el-table-column
v-if="isColumnVisible('amount')"
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">
<el-table-column
v-if="isColumnVisible('handlerName')"
prop="handlerName"
label="处理人"
min-width="100"
align="center"
>
<template #default="{ row }">{{ row.handlerName || '—' }}</template>
</el-table-column>
</el-table>
@@ -169,7 +255,7 @@
destroy-on-close
@closed="resetForm"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top" scroll-to-error>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="right" label-width="100px" class="pl-dialog-form" scroll-to-error>
<div v-if="dialogMode === 'disburse' && currentApplyRow" class="apply-summary-block">
<div class="apply-summary-title">关联申请</div>
<div class="apply-summary-grid">
@@ -293,7 +379,9 @@
</template>
<script setup lang="ts">
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { Check, Close, Delete, Edit, Money, Plus, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
vehicleApi,
@@ -322,6 +410,30 @@ const userOptions = ref<
>([])
const loading = ref(false)
const listColumnDefs = [
{ prop: 'applyDate', label: '申请时间' },
{ prop: 'warehouseInDate', label: '入库时间' },
{ prop: 'applicantName', label: '申请人' },
{ prop: 'unit', label: '单位' },
{ prop: 'quantity', label: '入库数' },
{ prop: 'applyAmount', label: '申请金额(元)' },
{ prop: 'applyStatus', label: '申请状态' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'disburseNo', label: '发放单号' },
{ prop: 'disburseDate', label: '发放时间' },
{ prop: 'amount', label: '发放金额(元)' },
{ prop: 'handlerName', label: '处理人' }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('pl-vehicle-urea-disburse-list-columns', listColumnDefs)
const submitLoading = ref(false)
const deleteLoadingMap = reactive<Record<string, boolean>>({})
const selectedRows = ref<VehicleUreaDisburseBoardItem[]>([])
@@ -329,6 +441,7 @@ const tableData = ref<VehicleUreaDisburseBoardItem[]>([])
const vehicleOptions = ref<Vehicle[]>([])
const applyDateRange = ref<string[]>([])
const disburseDateRange = ref<string[]>([])
const quickSearchKeyword = ref('')
const searchForm = reactive({
q: '',
@@ -465,7 +578,7 @@ async function fetchData() {
loading.value = true
try {
const res = await vehicleUreaDisburseApi.disburseBoard({
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
applyStatus: searchForm.applyStatus || undefined,
applyDateFrom: applyDateRange.value?.[0],
applyDateTo: applyDateRange.value?.[1],
@@ -483,6 +596,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -492,7 +606,20 @@ function handleReset() {
searchForm.applyStatus = ''
applyDateRange.value = []
disburseDateRange.value = []
handleSearch()
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function resetForm() {
+48 -14
View File
@@ -1,4 +1,4 @@
<!-- 车辆管理 -->
<!-- 全部车辆管理 -->
<template>
<div class="page-container">
<section class="panel">
@@ -61,6 +61,12 @@
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="车牌号"
@search="handleQuickSearch"
@clear="handleQuickSearchClear"
/>
<el-button class="btn-main" type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>新增车辆
</el-button>
@@ -200,12 +206,12 @@
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="960px"
width="1360px"
align-center
class="dialog-form-layout"
@close="handleDialogClose"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="top">
<el-form ref="formRef" :model="formData" :rules="formRules" label-position="right" label-width="100px" class="pl-dialog-form">
<div class="form-grid-3">
<el-form-item label="车牌号" prop="licensePlate">
<el-input v-model="formData.licensePlate" placeholder="请输入车牌号" maxlength="20" />
@@ -330,6 +336,7 @@
title="车辆证书"
upload-folder="pl/vehicle-cert"
:cert-name-options="certNameOptions"
layout="table"
/>
</el-form>
<template #footer>
@@ -346,7 +353,7 @@
<SystemViewDialog
v-model:visible="viewVisible"
:title="viewDetail?.licensePlate || '车辆详情'"
:title="viewDialogTitle"
:icon="Van"
:meta="viewHeroMeta"
:sections="viewSections"
@@ -374,7 +381,10 @@
:cert-list="viewDetail?.certList || []"
:license-plate="viewDetail?.licensePlate"
:trailer-no="viewDetail?.trailerNo"
:vehicle-id="viewDetail?.id"
:personnel-list="viewPersonnelList"
:check-self-vehicle-certs="viewDetail?.vehicleType === 'self'"
cert-layout="table"
/>
</template>
</SystemViewDialog>
@@ -383,6 +393,7 @@
<script setup lang="ts">
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { Check, Close, Delete, Edit, Plus, RefreshLeft, Search, Van } from '@element-plus/icons-vue'
import {
@@ -409,7 +420,8 @@ import { formatDateTime } from '@/utils/format'
import {
PL_VEHICLE_CERT_NAME_OPTIONS,
splitVehicleCertsByPart,
validateVehicleRequiredCerts
getSelfVehicleCertMissingSuffix,
validateVehicleCertNamesNotEmpty
} from '@/utils/pl-vehicle-cert'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
@@ -417,6 +429,7 @@ type CertListItem = CertRow & { _key?: string }
type CertFormListRef = InstanceType<typeof PlVehicleCertPartTabs>
const loading = ref(false)
const quickSearchKeyword = ref('')
const submitLoading = ref(false)
const dialogLoading = ref(false)
const vehicleList = ref<Vehicle[]>([])
@@ -523,6 +536,18 @@ const viewHeroMeta = computed(() => {
return parts.join(' · ')
})
const viewDialogTitle = computed(() => {
const plate = viewDetail.value?.licensePlate || '车辆详情'
if (!viewDetail.value || viewDetail.value.vehicleType !== 'self') return plate
return (
plate +
getSelfVehicleCertMissingSuffix({
certList: viewDetail.value.certList,
trailerNo: viewDetail.value.trailerNo
})
)
})
const viewSections = computed<ViewSection[]>(() => {
const d = viewDetail.value
if (!d) return []
@@ -713,21 +738,17 @@ function validateSubTables(): boolean {
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)
})
return validateVehicleCertNamesNotEmpty(
[headCerts, ...(hasTrailer ? [trailerCerts] : [])],
message => ElMessage.warning(message)
)
}
async function fetchData() {
loading.value = true
try {
const res = await vehicleApi.list({
q: searchForm.q || undefined,
q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
vehicleType: searchForm.vehicleType || undefined,
carrierId: searchForm.carrierId || undefined,
vehicleStatus: searchForm.vehicleStatus || undefined,
@@ -745,6 +766,7 @@ async function fetchData() {
}
function handleSearch() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
@@ -754,11 +776,23 @@ function handleReset() {
searchForm.vehicleType = ''
searchForm.carrierId = ''
searchForm.vehicleStatus = ''
quickSearchKeyword.value = ''
selectedRows.value = []
pagination.page = 1
fetchData()
}
function handleQuickSearch() {
pagination.page = 1
fetchData()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
pagination.page = 1
fetchData()
}
function handlePageChange() {
fetchData()
}
+148 -10
View File
@@ -1,7 +1,7 @@
<template>
<PublicMobileShell>
<div class="public-pl-site-page">
<van-nav-bar title="运输现场" :border="false" />
<van-nav-bar title="运输现场" :border="false" class="site-nav-bar" />
<van-loading v-if="loading" vertical class="page-loading">加载中...</van-loading>
<van-empty v-else-if="errorMessage" :description="errorMessage" />
@@ -11,11 +11,70 @@
<van-cell title="业务类型" :value="page.businessType || '—'" />
<van-cell title="日期" :value="page.date || '—'" />
<van-cell title="往来单位" :value="page.partnerCompany || '—'" />
<van-cell title="产品" :value="productDisplay" class="product-cell" />
<van-cell title="数量(吨)" :value="String(page.quantity ?? '—')" />
<van-cell title="车牌" :value="page.vehicleNo || '—'" />
<van-cell title="计划号" :value="page.planNo || '—'" />
<van-cell v-if="page.remark" title="备注" :value="page.remark" class="remark-cell" />
<van-cell v-if="page.remark" title="备注" :value="page.remark" class="multiline-cell" />
</van-cell-group>
<van-cell-group inset class="section-card" title="取货信息">
<van-cell title="取货地址" class="address-cell multiline-cell">
<template #value>
<div class="address-cell__content">
<span class="address-cell__text">{{ page.pickupAddress || '—' }}</span>
<van-button
v-if="pickupNavUrl"
size="mini"
type="primary"
round
class="address-nav-btn"
@click="openNavigation(pickupNavUrl)"
>
导航
</van-button>
</div>
</template>
</van-cell>
<van-cell title="取货时间" :value="formatDateTime(page.pickupTime)" />
<van-cell
title="注意事项"
:value="page.pickupNotes || '—'"
class="multiline-cell"
/>
</van-cell-group>
<van-cell-group inset class="section-card" title="送货信息">
<van-cell title="送货地址" class="address-cell multiline-cell">
<template #value>
<div class="address-cell__content">
<span class="address-cell__text">{{ deliveryAddressDisplay }}</span>
<van-button
v-if="destinationNavUrl"
size="mini"
type="primary"
round
class="address-nav-btn"
@click="openNavigation(destinationNavUrl)"
>
导航
</van-button>
</div>
</template>
</van-cell>
<van-cell title="送达时间" :value="formatDateTime(page.deliveryTime)" />
<van-cell
title="注意事项"
:value="page.destinationNotes || '—'"
class="multiline-cell"
/>
</van-cell-group>
<van-cell-group v-if="page.products.length" inset class="section-card" title="产品明细">
<van-cell
v-for="(item, idx) in page.products"
:key="idx"
:title="formatPublicTransportProductLine(item)"
class="product-cell"
/>
</van-cell-group>
<van-cell-group inset class="section-card" title="回执上传">
@@ -61,6 +120,8 @@
import { showFailToast, showSuccessToast } from 'vant'
import { publicPlSiteApi, type PublicTransportSitePage } from '@/api/pl/public-site'
import PublicMobileShell from '@/layouts/PublicMobileShell.vue'
import { buildAmapNavigationUrl } from '@/utils/amap/navigation'
import { formatPublicTransportProductLine } from '@/utils/pl-display'
import 'vant/es/toast/style'
import '@/styles/public-mobile-vant.scss'
@@ -76,12 +137,39 @@
const feedbackImages = ref<string[]>([])
const uploaderFiles = ref<UploaderFileListItem[]>([])
const productDisplay = computed(() => {
const deliveryAddressDisplay = computed(() => {
if (!page.value) return '—'
const spec = page.value.specification?.trim()
return spec ? `${page.value.product}(${spec})` : page.value.product
return page.value.destinationAddress || page.value.destinationName || '—'
})
const pickupNavUrl = computed(() => {
if (!page.value) return null
return buildAmapNavigationUrl(
page.value.pickupLng,
page.value.pickupLat,
page.value.pickupAddress || '取货点'
)
})
const destinationNavUrl = computed(() => {
if (!page.value) return null
return buildAmapNavigationUrl(
page.value.destinationLng,
page.value.destinationLat,
page.value.destinationAddress || page.value.destinationName || '送货点'
)
})
function openNavigation(url: string) {
window.location.href = url
}
function formatDateTime(value?: string) {
if (!value) return '—'
const normalized = value.replace('T', ' ')
return normalized.length > 16 ? normalized.slice(0, 16) : normalized
}
function resolveFileUrl(url: string) {
if (!url) return ''
if (/^https?:\/\//i.test(url)) return url
@@ -169,15 +257,65 @@
padding: 48px 0;
}
.site-nav-bar {
margin: -12px -16px 12px;
border-radius: 0 0 12px 12px;
overflow: hidden;
:deep(.van-nav-bar) {
background: #059669;
}
:deep(.van-nav-bar__title) {
color: #fff;
font-weight: 600;
}
}
.section-card {
margin-bottom: 12px;
margin-bottom: 10px;
}
.uploader-wrap {
padding: 12px 16px 16px;
padding: 8px 14px 12px;
}
.remark-cell :deep(.van-cell__value) {
.multiline-cell :deep(.van-cell__value) {
text-align: left;
line-height: 1.35;
white-space: normal;
}
.address-cell__content {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 8px;
justify-content: flex-start;
}
.address-cell__text {
line-height: 1.35;
color: #1e293b;
font-weight: 600;
}
.address-nav-btn {
flex-shrink: 0;
height: 22px;
padding: 0 8px;
font-size: 11px;
line-height: 22px;
}
.product-cell :deep(.van-cell__title) {
color: #1e293b;
font-weight: 600;
line-height: 1.35;
white-space: normal;
}
.product-cell :deep(.van-cell__value) {
display: none;
}
</style>
Loaded 100 of 253 files, more files were not shown because too many files have changed in this diff. Show more