This commit is contained in:
zlwu committed 2026-08-21 09:35:11 +08:00
1 parent b9ff9c9d67
commit 44749ae716
87 files changed
+35089 -21918

No files matched your search

@@ -207,6 +207,12 @@
<template v-if="!form.id">
<el-divider content-position="left">开票信息</el-divider>
<div class="form-grid-4">
<el-form-item label="快速识别">
<el-button class="btn-ghost" @click="openInvoicePasteDialog">
<el-icon><DocumentCopy /></el-icon>
快速识别
</el-button>
</el-form-item>
<el-form-item label="单位名称(章)">
<el-input
v-model="invoiceForm.companyName"
@@ -276,11 +282,49 @@
</div>
</template>
</el-dialog>
<el-dialog
v-model="invoicePasteDialog.visible"
title="快速识别开票信息"
width="640px"
class="dialog-form-layout"
align-center
append-to-body
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
@close="closeInvoicePasteDialog"
>
<div class="invoice-paste-panel">
<p class="invoice-paste-panel__tip">粘贴开票资料文本后点「识别并填入」,将覆盖新增表单中的开票信息对应字段</p>
<el-input
v-model="invoicePasteText"
type="textarea"
:rows="10"
placeholder="在此粘贴客户发来的开票资料…"
/>
</div>
<template #footer>
<div class="dialog-footer">
<el-button class="btn-ghost" @click="clearInvoicePaste">
<el-icon><Delete /></el-icon>
清空
</el-button>
<el-button class="btn-ghost" @click="closeInvoicePasteDialog">
<el-icon><Close /></el-icon>
取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleInvoicePasteRecognize">
<el-icon><Check /></el-icon>
识别并填入
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick } from 'vue'
import { Close, Check, Refresh } from '@element-plus/icons-vue'
import { Close, Check, Refresh, DocumentCopy, Delete } from '@element-plus/icons-vue'
import type { FormItemRule } from 'element-plus'
import { ElMessage } from 'element-plus'
import {
@@ -292,6 +336,10 @@ import {
import type { CustomerForm as CustomerFormType } from '@/api/crm/customerTypes'
import { addInvoiceInfo } from '@/api/crm/invoiceInfo'
import type { InvoiceInfoForm } from '@/api/crm/invoiceInfo'
import {
parseCrmInvoicePaste,
mergeInvoiceRemark
} from '@/utils/crm-invoice-paste-recognize'
import { useDictStore } from '@/store/modules/dict'
import { useUserStore } from '@/store/modules/user'
import { fetchUserOptions } from '@/api/admin'
@@ -475,6 +523,60 @@ function resetInvoiceForm() {
}
}
const invoicePasteDialog = reactive({
visible: false
})
const invoicePasteText = ref('')
function openInvoicePasteDialog() {
invoicePasteText.value = ''
invoicePasteDialog.visible = true
}
function closeInvoicePasteDialog() {
invoicePasteDialog.visible = false
invoicePasteText.value = ''
}
function clearInvoicePaste() {
invoicePasteText.value = ''
}
function handleInvoicePasteRecognize() {
const parsed = parseCrmInvoicePaste(invoicePasteText.value)
if (!parsed.filledCount && !parsed.remarkParts.length) {
ElMessage.warning(parsed.warnings[0] || '未能识别,请检查格式或手动填写')
return
}
const keys = [
'companyName',
'taxNo',
'companyAddress',
'legalRepresentative',
'agent',
'phoneFax',
'bankName',
'bankAccount'
] as const
for (const k of keys) {
const v = parsed[k]
if (v) invoiceForm.value[k] = v
}
if (parsed.remarkParts.length) {
invoiceForm.value.remark = mergeInvoiceRemark(invoiceForm.value.remark, parsed.remarkParts)
}
const tip =
`已识别 ${parsed.filledCount} 个字段` +
(parsed.remarkParts.length ? `,另有 ${parsed.remarkParts.length} 项写入备注` : '') +
(parsed.warnings.length ? `;${parsed.warnings.join(';')}` : '')
if (parsed.warnings.length) {
ElMessage.warning(tip)
} else {
ElMessage.success(tip)
}
closeInvoicePasteDialog()
}
async function syncInvoiceInfoAfterCreate(customerId: string | number) {
if (!hasInvoiceInfoFilled()) return
@@ -635,4 +737,11 @@ defineExpose({ open })
min-width: 0;
}
}
.invoice-paste-panel__tip {
margin: 0 0 10px;
font-size: 12px;
color: var(--mint-text-muted, #64748b);
line-height: 1.5;
}
</style>
@@ -134,6 +134,26 @@
@close="handleClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="crm-dialog-form">
<div class="invoice-paste-panel">
<div class="invoice-paste-panel__hd">
<span class="invoice-paste-panel__title">粘贴识别</span>
<span class="invoice-paste-panel__tip">粘贴开票资料文本后点「识别并填入」,将覆盖下方对应字段</span>
</div>
<el-input
v-model="pasteText"
type="textarea"
:rows="5"
placeholder="在此粘贴客户发来的开票资料…"
/>
<div class="invoice-paste-panel__actions">
<el-button class="btn-ghost" @click="clearPaste">
<el-icon><Delete /></el-icon>清空
</el-button>
<el-button class="btn-main" type="primary" @click="handleRecognizePaste">
<el-icon><DocumentCopy /></el-icon>识别并填入
</el-button>
</div>
</div>
<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" />
@@ -186,10 +206,14 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Plus, Edit, Delete, Star, Close, Check } from '@element-plus/icons-vue'
import { Plus, Edit, Delete, Star, Close, Check, DocumentCopy } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '@/utils/http'
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import {
parseCrmInvoicePaste,
mergeInvoiceRemark
} from '@/utils/crm-invoice-paste-recognize'
interface InvoiceInfoVO {
id: string | number
@@ -235,6 +259,7 @@ const dialog = ref({
})
const formRef = ref()
const pasteText = ref('')
const form = ref<InvoiceInfoForm>({
id: undefined,
customerId: '',
@@ -355,6 +380,44 @@ const handleClose = () => {
reset()
}
const clearPaste = () => {
pasteText.value = ''
}
const handleRecognizePaste = () => {
const parsed = parseCrmInvoicePaste(pasteText.value)
if (!parsed.filledCount && !parsed.remarkParts.length) {
ElMessage.warning(parsed.warnings[0] || '未能识别,请检查格式或手动填写')
return
}
const keys = [
'companyName',
'taxNo',
'companyAddress',
'legalRepresentative',
'agent',
'phoneFax',
'bankName',
'bankAccount'
] as const
for (const k of keys) {
const v = parsed[k]
if (v) form.value[k] = v
}
if (parsed.remarkParts.length) {
form.value.remark = mergeInvoiceRemark(form.value.remark, parsed.remarkParts)
}
const tip =
`已识别 ${parsed.filledCount} 个字段` +
(parsed.remarkParts.length ? `,另有 ${parsed.remarkParts.length} 项写入备注` : '') +
(parsed.warnings.length ? `;${parsed.warnings.join(';')}` : '')
if (parsed.warnings.length) {
ElMessage.warning(tip)
} else {
ElMessage.success(tip)
}
}
const reset = () => {
form.value = {
id: undefined,
@@ -371,6 +434,7 @@ const reset = () => {
remark: '',
}
formRef.value?.resetFields()
pasteText.value = ''
}
defineExpose({ getList })
@@ -384,4 +448,37 @@ defineExpose({ getList })
.invoice-empty-alert {
margin-bottom: 12px;
}
.invoice-paste-panel {
margin-bottom: 16px;
padding: 12px;
border: 1px dashed #6ee7b7;
border-radius: 8px;
background: var(--primary-bg, #ecfdf5);
}
.invoice-paste-panel__hd {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 8px;
margin-bottom: 8px;
}
.invoice-paste-panel__title {
font-weight: 600;
color: var(--primary-dark, #059669);
}
.invoice-paste-panel__tip {
font-size: 12px;
color: var(--mint-text-muted, #64748b);
}
.invoice-paste-panel__actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 10px;
}
</style>
+3 -5
View File
@@ -197,7 +197,7 @@
v-model="dialogVisible"
class="dialog-form-layout duty-day-dialog"
:title="`配置 · ${editingDate}`"
width="960px"
width="1100px"
align-center
destroy-on-close
:close-on-click-modal="false"
@@ -221,7 +221,7 @@
</div>
</div>
<template v-if="slot.enabled">
<div class="slot-fields-row">
<div class="form-grid-4 slot-fields-row">
<el-form-item label="值班时段">
<el-select
:model-value="slot.timeRangeValue"
@@ -229,7 +229,7 @@
allow-create
default-first-option
clearable
placeholder="选择或输入,如 09:00-17:00 / 18:00-次日07:00"
placeholder="选择或输入时段"
style="width: 100%"
@change="(v) => onTimeRangeChange(slot, v as string)"
>
@@ -241,8 +241,6 @@
/>
</el-select>
</el-form-item>
</div>
<div class="form-grid-3 slot-fields-row">
<el-form-item label="值班安排">
<el-select
v-model="slot.pendingUserId"
+2 -2
View File
@@ -74,10 +74,10 @@
if (menuType.value !== MenuTypeEnum.PORTAL) {
settingStore.switchMenuLayouts(MenuTypeEnum.PORTAL)
}
// 门户:开启多页签;首页固定 Tab
// 门户:开启多页签;首页固定 Tab(不可关闭)
settingStore.setWorkTab(true)
const home = homePath.value || HOME_PAGE_PATH || '/home'
if (home && !worktabStore.opened.some((t) => t.path === home)) {
if (home) {
worktabStore.openTab(
{
path: home,
+129 -64
View File
@@ -1,12 +1,8 @@
<!-- eslint-disable vue/no-v-model-argument -->
<!-- 运输计划合同完善:仅缺合同计划,行内展开分侧保存 -->
<template>
<div class="page-container">
<section class="panel">
<div class="panel-top pl-plan-contract-panel-top">
<h2 class="title-bar-heading">运输计划合同完善</h2>
<span class="pl-plan-contract-stat">待完善 {{ pagination.total }} 条</span>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
@@ -77,6 +73,7 @@
<el-button class="btn-ghost" :loading="loading" @click="fetchData">
<el-icon><Refresh /></el-icon>刷新
</el-button>
<span class="pl-plan-contract-stat">待完善 {{ pagination.total }} 条</span>
</div>
</div>
@@ -92,7 +89,7 @@
@expand-change="handleExpandChange"
@row-dblclick="handleView"
>
<el-table-column type="expand" min-width="48">
<el-table-column type="expand" class-name="pl-contract-expand-col" width="1">
<template #default="{ row }">
<div class="pl-contract-expand" v-if="expandDraft[row.id]">
<div class="pl-contract-expand__grid">
@@ -200,6 +197,52 @@
{{ getPlanTypeLabel(row.planType) }}
</template>
</el-table-column>
<el-table-column
label="合同缺口"
min-width="260"
align="left"
header-align="left"
class-name="col-name-wrap"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
<div class="pl-gap-contract-cell">
<div class="pl-plan-contract-cell">
<div
v-for="(line, index) in planContractLines(row)"
:key="`${line.side}-${line.no}-${index}`"
class="pl-plan-contract-cell__row"
>
<span class="pl-plan-contract-cell__side">{{ line.side }}</span>
<span
class="pl-plan-contract-cell__no"
:class="{ 'is-pending': line.pending }"
>{{ line.no }}</span>
</div>
</div>
<el-button
link
type="warning"
class="pl-gap-contract-cell__action"
@click.stop="toggleExpand(row)"
>
<el-icon><EditPen /></el-icon>
{{ isExpanded(row.id) ? '收起' : '补充' }}
</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="followStatus" label="跟进状态" min-width="110" align="center">
<template #default="{ row }">
<el-tag
:type="getPlPlanFollowStatusTagType(row.followStatus)"
size="small"
effect="dark"
>
{{ getPlPlanFollowStatusLabel(row.followStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="supplierName"
label="供方"
@@ -234,7 +277,7 @@
</el-table-column>
<el-table-column
label="产品名称"
min-width="240"
min-width="160"
align="left"
class-name="col-name-wrap"
label-class-name="col-name-wrap"
@@ -268,39 +311,6 @@
<PlPlanListProductCell :products="row.products" field="actualQty" />
</template>
</el-table-column>
<el-table-column label="合同缺口" min-width="130" align="center">
<template #default="{ row }">
<el-tag :type="gapTag(row).type" size="small" effect="dark">
{{ gapTag(row).text }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="followStatus" label="跟进状态" min-width="110" align="center">
<template #default="{ row }">
<el-tag
:type="getPlPlanFollowStatusTagType(row.followStatus)"
size="small"
effect="dark"
>
{{ getPlPlanFollowStatusLabel(row.followStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" min-width="200" align="center">
<template #default="{ row }">
<el-button
link
type="warning"
@click="toggleExpand(row)"
>
<el-icon><EditPen /></el-icon>
{{ isExpanded(row.id) ? '收起' : '展开补充' }}
</el-button>
<el-button link type="primary" @click="handleView(row)">
<el-icon><View /></el-icon>查看计划
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination list-pager-bar">
@@ -327,7 +337,7 @@
</template>
<script setup lang="ts">
import { Check, EditPen, Refresh, RefreshLeft, Search, View } from '@element-plus/icons-vue'
import { Check, EditPen, Refresh, RefreshLeft, Search } from '@element-plus/icons-vue'
import {
planApi,
type PlanContractGapItem,
@@ -343,6 +353,7 @@ import {
getPlPlanFollowStatusLabel,
getPlPlanFollowStatusTagType
} from '@/utils/pl-display'
import { planContractLines } from '@/utils/pl-plan-contract-display'
import { refreshMenuBadges } from '@/utils/menu-badge-registry'
import { useMenuStore } from '@/store/modules/menu'
@@ -395,20 +406,6 @@ function linksFingerprint(links: PlanContractLink[]) {
.join(',')
}
function gapTag(row: PlanContractGapItem) {
const sides = row.gapSides || []
if (sides.includes('supplier') && sides.includes('demander')) {
return { text: '两侧均缺', type: 'danger' as const }
}
if (sides.includes('supplier')) {
return { text: '缺供方合同', type: 'warning' as const }
}
if (sides.includes('demander')) {
return { text: '缺需方合同', type: 'warning' as const }
}
return { text: '—', type: 'info' as const }
}
function getPlanTypeLabel(type?: string | null) {
const map: Record<string, string> = {
direct: '直销单',
@@ -422,11 +419,11 @@ function partySubText(row: PlanContractGapItem, side: 'supplier' | 'demander') {
if (side === 'supplier') {
if (row.supplierContractExempt) return '内置 · 合同可免'
const n = row.supplierContracts?.length || 0
return n ? `已关联 ${n} 份` : '未关联'
return n ? `已关联${n}份合同` : '未关联合同'
}
if (row.demanderContractExempt) return '内置 · 合同可免'
const n = row.demanderContracts?.length || 0
return n ? `已关联 ${n} 份` : '未关联'
return n ? `已关联${n}份合同` : '未关联合同'
}
function sideBoxClass(row: PlanContractGapItem, side: 'supplier' | 'demander') {
@@ -647,18 +644,24 @@ onMounted(() => {
</script>
<style scoped lang="scss">
.pl-plan-contract-panel-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 20px 12px;
border-bottom: 1px solid #eef2f6;
.pl-plan-list-toolbar-row {
flex-wrap: nowrap;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
> .list-toolbar-date-shortcuts,
> .quick-search-input,
> .quick-search-group,
> .el-button,
> .pl-plan-contract-stat {
flex-shrink: 0;
}
}
.pl-plan-contract-stat {
display: inline-flex;
align-items: center;
margin-left: auto;
padding: 4px 10px;
border-radius: 8px;
font-size: 12px;
@@ -755,4 +758,66 @@ onMounted(() => {
gap: 8px;
margin-top: 10px;
}
:deep(.pl-contract-expand-col) {
.cell {
padding: 0 !important;
}
.el-table__expand-icon {
display: none;
}
}
.pl-gap-contract-cell {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
width: 100%;
}
.pl-gap-contract-cell__action {
flex-shrink: 0;
margin-top: 0;
}
.pl-plan-contract-cell {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
min-width: 0;
flex: 1;
text-align: left;
}
.pl-plan-contract-cell__row {
display: flex;
align-items: baseline;
gap: 6px;
width: 100%;
min-width: 0;
font-size: 13px;
line-height: 1.5;
color: var(--text, #1e293b);
}
.pl-plan-contract-cell__side {
flex-shrink: 0;
font-weight: 600;
color: var(--mint-text-muted, #64748b);
}
.pl-plan-contract-cell__no {
min-width: 0;
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
&.is-pending {
font-weight: 600;
color: #d97706;
}
}
</style>
+40 -53
View File
@@ -268,29 +268,15 @@
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('contractSummary')"
label="合同"
min-width="220"
align="left"
header-align="left"
class-name="col-name-wrap"
label-class-name="col-name-wrap"
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="160"
show-overflow-tooltip
align="center"
>
<template #default="{ row }">
<div
v-if="planContractLines(row).length"
class="pl-plan-contract-cell"
>
<div
v-for="(line, index) in planContractLines(row)"
:key="`${line.side}-${line.no}-${index}`"
class="pl-plan-contract-cell__row"
>
<span class="pl-plan-contract-cell__side">{{ line.side }}</span>
<span class="pl-plan-contract-cell__no">{{ line.no }}</span>
</div>
</div>
<span v-else class="pl-plan-contract-cell__empty">—</span>
{{ formatPlVehiclePlateDisplay(row.licensePlate, row.trailerNo) }}
</template>
</el-table-column>
<el-table-column
@@ -374,18 +360,6 @@
<PlPlanListCertCell :products="row.products" compact hide-product />
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('licensePlate')"
prop="licensePlate"
label="车牌号"
min-width="160"
show-overflow-tooltip
align="center"
>
<template #default="{ row }">
{{ formatPlVehiclePlateDisplay(row.licensePlate, row.trailerNo) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('personnelSummary')"
prop="personnelSummary"
@@ -405,6 +379,31 @@
min-width="100"
align="center"
/>
<el-table-column
v-if="isColumnVisible('contractSummary')"
label="合同"
min-width="220"
align="left"
header-align="left"
class-name="col-name-wrap"
label-class-name="col-name-wrap"
>
<template #default="{ row }">
<div class="pl-plan-contract-cell">
<div
v-for="(line, index) in planContractLines(row)"
:key="`${line.side}-${line.no}-${index}`"
class="pl-plan-contract-cell__row"
>
<span class="pl-plan-contract-cell__side">{{ line.side }}</span>
<span
class="pl-plan-contract-cell__no"
:class="{ 'is-pending': line.pending }"
>{{ line.no }}</span>
</div>
</div>
</template>
</el-table-column>
</ListTable>
<ListColumnStatsRow
ref="planStatsRowRef"
@@ -568,6 +567,7 @@ import ListToolbarDateShortcuts from '@/components/core/list-toolbar-date-shortc
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { useListDateShortcut } from '@/composables/useListDateShortcut'
import { canBatchPrintSlip } from '@/utils/pl-batch-print'
import { planContractLines } from '@/utils/pl-plan-contract-display'
import {
canPrintSlipCommand,
formatPlVehiclePlateDisplay,
@@ -607,7 +607,7 @@ const listColumnDefs = [
{ prop: 'planType', label: '单据类型' },
{ prop: 'supplierName', label: '供方' },
{ prop: 'customerName', label: '需方' },
{ prop: 'contractSummary', label: '合同' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'productName', label: '产品名称' },
{ prop: 'productSpec', label: '型号/规格' },
{ prop: 'productPackageSpec', label: '包装规格' },
@@ -615,9 +615,9 @@ const listColumnDefs = [
{ prop: 'productPlanQty', label: '计划数量' },
{ prop: 'productActualQty', label: '实际数量' },
{ prop: 'planAttachments', label: '计划附件' },
{ prop: 'licensePlate', label: '车牌号' },
{ prop: 'personnelSummary', label: '人员' },
{ prop: 'followUserName', label: '跟进人', defaultVisible: false }
{ prop: 'followUserName', label: '跟进人', defaultVisible: false },
{ prop: 'contractSummary', label: '合同' }
]
const {
@@ -785,20 +785,6 @@ function getPlanTypeLabel(type?: string | null) {
return map[type] || type
}
/** 合同列:供/需合同各占一行展示 */
function planContractLines(plan: Plan): Array<{ side: string; no: string }> {
const lines: Array<{ side: string; no: string }> = []
for (const item of plan.supplierContracts || []) {
const no = item.contractNo || item.contractId
if (no) lines.push({ side: '供', no: String(no) })
}
for (const item of plan.demanderContracts || []) {
const no = item.contractNo || item.contractId
if (no) lines.push({ side: '需', no: String(no) })
}
return lines
}
async function fetchData() {
loading.value = true
try {
@@ -1190,9 +1176,10 @@ async function openArrangeFromQuery() {
white-space: normal;
word-break: break-word;
overflow-wrap: anywhere;
}
.pl-plan-contract-cell__empty {
color: var(--mint-text-muted, #64748b);
&.is-pending {
font-weight: 600;
color: #d97706;
}
}
</style>
@@ -260,7 +260,12 @@
<el-input v-model="formData.licensePlate" placeholder="请输入车牌号" maxlength="20" />
</el-form-item>
<el-form-item label="挂车号" prop="trailerNo">
<el-input v-model="formData.trailerNo" placeholder="请输入挂车号" maxlength="20" clearable />
<el-input
v-model="formData.trailerNo"
placeholder="必填;无挂车可填「货拉拉」"
maxlength="20"
clearable
/>
</el-form-item>
<el-form-item label="载重(吨)" prop="loadCapacity">
<el-input-number
@@ -621,6 +626,7 @@ const formData = reactive({
const formRules = {
licensePlate: [{ required: true, message: '请输入车牌号', trigger: 'blur' }],
trailerNo: [{ required: true, message: '请输入挂车号', trigger: 'blur' }],
vehicleStatus: [{ required: true, message: '请选择车辆状态', trigger: 'change' }],
carrierId: [{ required: true, message: '请选择承运商', trigger: 'change' }]
}
+9 -3
View File
@@ -268,7 +268,12 @@
<el-input v-model="formData.licensePlate" placeholder="请输入车牌号" maxlength="20" />
</el-form-item>
<el-form-item label="挂车号" prop="trailerNo">
<el-input v-model="formData.trailerNo" placeholder="请输入挂车号" maxlength="20" clearable />
<el-input
v-model="formData.trailerNo"
placeholder="必填;无挂车可填「货拉拉」"
maxlength="20"
clearable
/>
</el-form-item>
<el-form-item label="载重(吨)" prop="loadCapacity">
<el-input-number
@@ -398,7 +403,7 @@
:cert-name-options="certNameOptions"
layout="table"
:enforce-self-vehicle-required="false"
:enforce-carrier-vehicle-required="false"
enforce-carrier-vehicle-required
/>
</el-form>
<template #footer>
@@ -640,7 +645,8 @@ const formData = reactive({
})
const formRules = {
licensePlate: [{ required: true, message: '请输入车牌号', trigger: 'blur' }]
licensePlate: [{ required: true, message: '请输入车牌号', trigger: 'blur' }],
trailerNo: [{ required: true, message: '请输入挂车号', trigger: 'blur' }]
}
function getStatusTagType(status: string) {
+2 -1
View File
@@ -219,7 +219,8 @@
备用金发放: MENU_BADGE_CODES.BX_ADVANCE_ISSUE,
报销审批: MENU_BADGE_CODES.BX_REIMBURSE_APPROVE,
支付发放冲抵: MENU_BADGE_CODES.BX_PAYMENT,
待我审批: MENU_BADGE_CODES.BX_VIEW_TODO
待我审批: MENU_BADGE_CODES.BX_VIEW_TODO,
运输计划合同完善: MENU_BADGE_CODES.PL_PLAN_CONTRACT
}
const router = useRouter()
+11 -6
View File
@@ -460,17 +460,22 @@
margin-top: 10px;
}
.person {
padding: 4px 9px;
font-size: 12px;
color: #6b7b74;
background: #f3f7f5;
border: 1px solid #dcebe4;
padding: 5px 11px;
font-size: 13px;
font-weight: 600;
line-height: 1.3;
color: #fff;
background: #0a9f6d;
border: none;
border-radius: 999px;
box-shadow: 0 1px 2px rgba(10, 159, 109, 0.25);
}
.person.empty {
font-weight: 400;
color: #8a9a93;
background: transparent;
border-style: dashed;
border: 1px dashed #c5d5ce;
box-shadow: none;
}
.field {
display: block;
File diff suppressed because it is too large. Load diff
+106 -27
View File
@@ -45,7 +45,11 @@
<el-button class="btn-ghost" :loading="loading" @click="handleReset"
><el-icon><RefreshLeft /></el-icon>重置</el-button
>
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch"
<el-button
class="btn-main"
type="primary"
:loading="loading"
@click="handleSearch"
><el-icon><Search /></el-icon>搜索</el-button
>
</div>
@@ -55,7 +59,7 @@
<div class="list-toolbar">
<div class="list-toolbar-row">
<ListQuickSearchInput
<ListQuickSearchInput
v-model="quickSearchKeyword"
placeholder="账号/姓名/手机号"
@search="handleQuickSearch"
@@ -105,7 +109,8 @@
</div>
<div class="list-body list-wrap">
<ListTable table-key="system-user"
<ListTable
table-key="system-user"
class="list-table"
:data="tableData"
v-loading="loading"
@@ -114,7 +119,12 @@
@selection-change="handleSelectionChange"
@row-dblclick="handleView"
>
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
<el-table-column
type="selection"
min-width="55"
align="center"
header-align="center"
/>
<el-table-column
type="index"
label="序号"
@@ -151,7 +161,27 @@
header-align="center"
>
<template #default="{ row }">
{{ formatUserDeptDisplay(row) }}
{{ row.deptName || '—' }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('roles')"
prop="roles"
label="角色"
min-width="180"
align="center"
header-align="center"
>
<template #default="{ row }">
<template v-if="resolveUserRoles(row).length">
<template v-for="(role, idx) in resolveUserRoles(row)" :key="role.id">
<span v-if="idx > 0" class="user-role-sep">、</span>
<el-link type="primary" @click.stop="handleOpenRole(role.id)">{{
role.label
}}</el-link>
</template>
</template>
<span v-else>—</span>
</template>
</el-table-column>
<el-table-column
@@ -191,9 +221,9 @@
</ListTable>
<div class="pagination list-pager-bar">
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<div class="list-pager-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-pagination
:current-page="pagination.page"
@@ -324,14 +354,38 @@
删除
</el-button>
</template>
<template #userRoles>
<div class="user-view-roles">
<template v-if="viewRow && resolveUserRoles(viewRow).length">
<el-tag
v-for="role in resolveUserRoles(viewRow)"
:key="role.id"
type="info"
size="small"
class="user-role-tag"
@click="handleOpenRole(role.id)"
>
{{ role.label }}
</el-tag>
</template>
<span v-else>—</span>
</div>
</template>
</SystemViewDialog>
<RoleManageDialogs ref="roleDialogsRef" />
</section>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from 'vue'
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import {
canDeleteByCreator,
filterDeletableByCreator,
allRowsDeletableByCreator,
CREATOR_DELETE_TIP
} from '@/utils/creator-delete'
import { syncViewRowFromFlatList } from '@/utils/view-dialog-refresh'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
@@ -346,6 +400,7 @@
Delete
} from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import RoleManageDialogs from '@/components/business/role-manage-dialogs/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'
@@ -359,12 +414,12 @@
fetchDeptTree,
fetchRoleOptions
} from '@/api/admin'
import { buildPathLabelMapFromTreeRoots } from '@/utils/tree-path'
import { useDictStore } from '@/store/modules/dict'
import { buildPathLabelMapFromTreeRoots } from '@/utils/tree-path'
import { useDictStore } from '@/store/modules/dict'
defineOptions({ name: 'SystemUser' })
defineOptions({ name: 'SystemUser' })
const dictStore = useDictStore()
const dictStore = useDictStore()
const roleOptions = ref<Array<{ label: string; value: string }>>([])
@@ -396,13 +451,20 @@ const dictStore = useDictStore()
return deptPathLabelMap.value.get(id) || row.deptName || '—'
}
function formatUserRoleDisplay(row: Api.User.UserItem) {
function resolveUserRoles(row: Api.User.UserItem) {
const ids = row.roleIds || []
if (!ids.length) return '—'
const labels = ids
.map((id) => roleOptions.value.find((o) => o.value === id)?.label)
.filter((name): name is string => Boolean(name))
return labels.length ? labels.join('、') : '—'
return ids
.map((id) => {
const label = roleOptions.value.find((o) => o.value === id)?.label
return label ? { id, label } : null
})
.filter((item): item is { id: string; label: string } => Boolean(item))
}
const roleDialogsRef = ref<InstanceType<typeof RoleManageDialogs> | null>(null)
function handleOpenRole(roleId: string) {
roleDialogsRef.value?.openView(roleId)
}
const searchForm = reactive<{ keyword: string; status: string }>({ keyword: '', status: '' })
@@ -415,18 +477,14 @@ const dictStore = useDictStore()
const listColumnDefs = [
{ prop: 'userNo', label: '用户账号' },
{ prop: 'deptName', label: '所属组织' },
{ prop: 'roles', label: '角色' },
{ prop: 'phoneNo', label: '手机号' },
{ prop: 'status', label: '状态' },
{ prop: 'createTime', label: '创建时间', defaultVisible: false }
]
const {
visibleColumns,
checkAll,
isIndeterminate,
isColumnVisible,
resetToDefault
} = usePersistedListColumns('system-user-list-columns', listColumnDefs)
const { visibleColumns, checkAll, isIndeterminate, isColumnVisible, resetToDefault } =
usePersistedListColumns('system-user-list-columns', listColumnDefs)
const dialogVisible = ref(false)
const dialogTitle = ref('新增用户')
@@ -722,9 +780,14 @@ const dictStore = useDictStore()
long: false
},
{ label: '创建时间', value: row.createTime, long: false },
{ label: '角色', value: formatUserRoleDisplay(row), long: false },
{ label: '备注', value: row.remark, long: false }
]
},
{
title: '角色',
titleStyle: 'field' as const,
fields: [],
slot: 'userRoles'
}
]
})
@@ -823,4 +886,20 @@ const dictStore = useDictStore()
margin-bottom: 0;
}
}
.user-role-tag {
margin: 2px 4px;
cursor: pointer;
}
.user-role-sep {
color: var(--mint-text-muted, var(--el-text-color-secondary));
}
.user-view-roles {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
</style>