提交
This commit is contained in:
1 parent
33c038b8d9
commit
54b27a3f0e
253 files changed
+30060
-14298
No files matched your search
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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('请先勾选一条节点')
|
||||
|
||||
@@ -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' }"
|
||||
>
|
||||
|
||||
@@ -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' }"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
@@ -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' }"
|
||||
>
|
||||
@@ -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')"
|
||||
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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' }"
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
Reference in new issue
Block a user