提交
This commit is contained in:
1 parent
33c038b8d9
commit
54b27a3f0e
253 files changed
+30060
-14298
No files matched your search
@@ -22,6 +22,8 @@
|
||||
import { toggleTransition } from './utils/ui/animation'
|
||||
import { checkStorageCompatibility } from './utils/storage'
|
||||
import { initializeTheme } from './hooks/core/useTheme'
|
||||
import { refreshFilterCollapse, setupFilterCollapseObserver } from './utils/ui/filter-collapse'
|
||||
import { setupListTableRowSelect } from './utils/ui/list-table-row-select'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { language } = storeToRefs(userStore)
|
||||
@@ -33,6 +35,8 @@
|
||||
|
||||
let buttonIconObserver: MutationObserver | null = null
|
||||
let panelTitleObserver: MutationObserver | null = null
|
||||
let filterCollapseObserver: MutationObserver | null = null
|
||||
let teardownListTableRowSelect: (() => void) | null = null
|
||||
|
||||
const iconClassByText: Array<[RegExp, string]> = [
|
||||
[/(新增|新建|创建|添加)/, 'auto-icon-plus'],
|
||||
@@ -147,6 +151,7 @@
|
||||
systemUpgrade()
|
||||
refreshButtonAutoIcons()
|
||||
refreshPanelTitleIcons()
|
||||
refreshFilterCollapse()
|
||||
|
||||
buttonIconObserver = new MutationObserver(() => {
|
||||
refreshButtonAutoIcons()
|
||||
@@ -163,6 +168,9 @@
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
|
||||
filterCollapseObserver = setupFilterCollapseObserver()
|
||||
teardownListTableRowSelect = setupListTableRowSelect()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -170,5 +178,9 @@
|
||||
buttonIconObserver = null
|
||||
panelTitleObserver?.disconnect()
|
||||
panelTitleObserver = null
|
||||
filterCollapseObserver?.disconnect()
|
||||
filterCollapseObserver = null
|
||||
teardownListTableRowSelect?.()
|
||||
teardownListTableRowSelect = null
|
||||
})
|
||||
</script>
|
||||
@@ -394,7 +394,11 @@ export interface Plan {
|
||||
notifyMessage?: string
|
||||
followUserId?: string
|
||||
followUserName?: string
|
||||
hazardCertReady?: number
|
||||
hazardCertReadyBy?: string
|
||||
hazardCertReadyTime?: string
|
||||
remark?: string
|
||||
createBy?: string
|
||||
createTime: string
|
||||
products?: PlanProduct[]
|
||||
}
|
||||
@@ -522,6 +526,8 @@ export const planApi = {
|
||||
id: string,
|
||||
products: Array<{ id: string; purchaseCertUrl?: string; transportCertUrl?: string }>
|
||||
) => request.patch<Plan>(`/transport/plan/${id}/product-certs`, { products }),
|
||||
setHazardCertReady: (id: string, ready: boolean) =>
|
||||
request.patch<Plan>(`/transport/plan/${id}/hazard-cert-ready`, { ready }),
|
||||
cancel: (id: string) => request.post<Plan>(`/transport/plan/${id}/cancel`),
|
||||
getArrangeInfo: (id: string) => request.get<PlanArrangeInfo>(`/transport/plan/${id}/arrange-info`),
|
||||
archive: (id: string, data?: PlanArchiveBody) =>
|
||||
@@ -769,6 +775,7 @@ export interface FreightSettlement {
|
||||
transports?: Transport[]
|
||||
plans?: SettlementPlanSummary[]
|
||||
approvalFlow?: SettlementApprovalFlowItem[]
|
||||
approvalHistory?: SettlementApprovalFlowItem[]
|
||||
vouchers?: RemittanceVoucher[]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
import { publicPlGet, publicPlPost, publicPlUpload } from '@/utils/http/public-pl'
|
||||
import type { PublicTransportProduct } from '@/api/pl/public-transport'
|
||||
|
||||
export interface PublicTransportSitePage {
|
||||
batchNo?: string
|
||||
businessType: string
|
||||
date?: string
|
||||
partnerCompany: string
|
||||
product: string
|
||||
specification?: string
|
||||
quantity: number
|
||||
vehicleNo: string
|
||||
planNo?: string
|
||||
status?: string
|
||||
remark?: string
|
||||
pickupAddress?: string
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupTime?: string
|
||||
pickupNotes?: string
|
||||
destinationName?: string
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryTime?: string
|
||||
destinationNotes?: string
|
||||
products: PublicTransportProduct[]
|
||||
siteFeedbackRemark?: string
|
||||
siteFeedbackImages: string[]
|
||||
siteFeedbackTime?: string
|
||||
readOnly: boolean
|
||||
voided: boolean
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@ export interface PublicTransportProduct {
|
||||
packageSpec?: string
|
||||
unit?: string
|
||||
quantity?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface PublicTransportPersonnel {
|
||||
personnelName: string
|
||||
phoneNo?: string
|
||||
role?: string
|
||||
}
|
||||
|
||||
export interface PublicTransportPage {
|
||||
@@ -15,12 +22,18 @@ export interface PublicTransportPage {
|
||||
pickupAddress?: string
|
||||
pickupLng?: number | null
|
||||
pickupLat?: number | null
|
||||
pickupTime?: string
|
||||
pickupNotes?: string
|
||||
destinationName?: string
|
||||
destinationAddress?: string
|
||||
destinationLng?: number | null
|
||||
destinationLat?: number | null
|
||||
deliveryTime?: string
|
||||
destinationNotes?: string
|
||||
licensePlate?: string
|
||||
driverName?: string
|
||||
driverPhone?: string
|
||||
otherPersonnel?: PublicTransportPersonnel[]
|
||||
products: PublicTransportProduct[]
|
||||
feedbackRemark?: string
|
||||
feedbackImages: string[]
|
||||
|
||||
@@ -125,7 +125,7 @@ body {
|
||||
// 盒子默认边框
|
||||
.page-content {
|
||||
border: 1px solid var(--mint-border) !important;
|
||||
border-radius: var(--mint-radius) !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: var(--mint-shadow) !important;
|
||||
}
|
||||
|
||||
@@ -359,8 +359,8 @@ body {
|
||||
|
||||
.panel {
|
||||
background: var(--mint-surface);
|
||||
border: 1px solid var(--mint-border);
|
||||
border-radius: var(--mint-radius);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: var(--mint-shadow);
|
||||
/* 不要用 overflow:hidden:会导致 el-table 右侧固定列(fixed="right")无法正确粘贴/层级错乱 */
|
||||
overflow: visible;
|
||||
@@ -941,7 +941,11 @@ body {
|
||||
.filter-block {
|
||||
padding: 12px 16px 10px;
|
||||
background: var(--mint-surface);
|
||||
border-bottom: 1px solid var(--mint-border);
|
||||
border-bottom: none;
|
||||
|
||||
&.is-filter-collapsed {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-hint {
|
||||
@@ -1066,7 +1070,7 @@ body {
|
||||
.toolbar {
|
||||
padding: 0;
|
||||
background: var(--mint-table-head);
|
||||
border-bottom: 1px solid var(--mint-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-toolbar-row,
|
||||
@@ -1077,6 +1081,7 @@ body {
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
background: var(--mint-table-head);
|
||||
}
|
||||
|
||||
/* 列表工具栏按钮间距统一由 gap 控制,清除 Element 默认相邻按钮 margin */
|
||||
@@ -1093,15 +1098,93 @@ body {
|
||||
.toolbar-left {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
min-height: 36px;
|
||||
padding: 0 10px 0 0;
|
||||
margin-right: 4px;
|
||||
border-right: 1px solid var(--mint-border);
|
||||
border-right: none;
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-list-filter-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin-right: 8px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--mint-text-muted);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: currentColor;
|
||||
opacity: 0.95;
|
||||
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3'/%3E%3C/svg%3E")
|
||||
center / contain no-repeat;
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolygon points='22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3'/%3E%3C/svg%3E")
|
||||
center / contain no-repeat;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--mint-primary-dark);
|
||||
background: var(--mint-primary-bg);
|
||||
}
|
||||
|
||||
&[aria-expanded='true'] {
|
||||
color: var(--mint-primary-dark);
|
||||
background: var(--mint-primary-bg);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--mint-primary);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-search-group {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-right: 8px;
|
||||
gap: 6px;
|
||||
|
||||
.quick-search-input {
|
||||
width: 280px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.btn-list-filter-toggle {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.quick-search-input:not(.quick-search-group .quick-search-input) {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.quick-search-input .quick-search-icon {
|
||||
cursor: pointer;
|
||||
color: var(--mint-text-muted);
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--mint-primary-dark);
|
||||
}
|
||||
}
|
||||
|
||||
.sel-num {
|
||||
color: var(--mint-primary-dark);
|
||||
font-weight: 700;
|
||||
@@ -1113,7 +1196,7 @@ body {
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
border-radius: 0 0 var(--mint-radius) var(--mint-radius);
|
||||
border-radius: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -1131,9 +1214,9 @@ body {
|
||||
gap: 8px 12px;
|
||||
margin-top: 0;
|
||||
padding: 10px 16px 12px;
|
||||
border-top: 1px solid var(--mint-border);
|
||||
border-top: none;
|
||||
background: var(--mint-surface);
|
||||
border-radius: 0 0 var(--mint-radius) var(--mint-radius);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* 分页条左侧说明文案(BX range / 分类 total-count 等)与右侧控件区分 */
|
||||
@@ -1147,14 +1230,14 @@ body {
|
||||
justify-content: flex-end;
|
||||
margin-top: 0;
|
||||
padding: 10px 16px 12px;
|
||||
border-top: 1px solid var(--mint-border);
|
||||
border-top: none;
|
||||
background: var(--mint-surface);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
background: var(--mint-surface);
|
||||
border: 1px solid var(--mint-border);
|
||||
border-radius: var(--mint-radius);
|
||||
border-radius: 0;
|
||||
box-shadow: var(--mint-shadow);
|
||||
}
|
||||
|
||||
@@ -1162,7 +1245,7 @@ body {
|
||||
margin-bottom: 14px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--mint-border);
|
||||
border-radius: var(--mint-radius);
|
||||
border-radius: 0;
|
||||
background: var(--mint-primary-bg);
|
||||
}
|
||||
|
||||
@@ -1179,7 +1262,7 @@ body {
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
border-radius: var(--mint-radius);
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@@ -1243,6 +1326,14 @@ body {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* 弹窗表单:栅格块与后续表单项/栅格块之间保留行间距(与模块弹窗 form-grid row-gap 一致) */
|
||||
.el-dialog.dialog-form-layout .el-form {
|
||||
[class*='form-grid-'] + .el-form-item,
|
||||
[class*='form-grid-'] + [class*='form-grid-'] {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表单栅格内 el-input-number 默认约 150px,须拉满与 el-input / el-select 同宽 */
|
||||
.el-dialog.dialog-form-layout .el-form-item .el-input-number,
|
||||
.page-container .el-form-item .el-input-number,
|
||||
@@ -1288,11 +1379,11 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* 标准 CRUD 弹窗:相对视口上下各保留至少 50px 可视留白,正文区纵向滚动(不占满视口高度) */
|
||||
/* 标准 CRUD 弹窗:相对视口上下各 20px 留白,正文区纵向滚动 */
|
||||
.el-dialog.dialog-form-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 100px);
|
||||
max-height: var(--dialog-max-height, calc(100vh - 40px));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1300,6 +1391,14 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-dialog.dialog-form-layout .el-dialog__headerbtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.el-dialog.dialog-form-layout .el-dialog__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -1321,13 +1420,18 @@ body {
|
||||
backdrop-filter: none !important;
|
||||
|
||||
.el-overlay-dialog {
|
||||
margin-top: 16px;
|
||||
margin-top: 0;
|
||||
margin-bottom: auto;
|
||||
padding-top: var(--dialog-viewport-inset, 20px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.el-dialog.dialog-form-layout,
|
||||
.el-dialog.order-module-dialog {
|
||||
max-width: calc(100vw - 48px);
|
||||
max-height: calc(
|
||||
100vh - var(--dialog-viewport-inset, 20px) * 2
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1365,6 +1469,12 @@ body {
|
||||
background: var(--mint-surface);
|
||||
}
|
||||
|
||||
.app-layout-portal #app-sidebar .layout-sidebar {
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--mint-border) !important;
|
||||
background: var(--mint-surface);
|
||||
}
|
||||
|
||||
#app-sidebar .menu-left .header {
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
@@ -1383,6 +1493,14 @@ body {
|
||||
|
||||
/* 弹窗 / 抽屉内键值展示:上下布局、深色正文(对齐 zkxw Token 链) */
|
||||
:is(.el-dialog, .system-view-dialog, .el-drawer) {
|
||||
.el-form-item__label {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.view-kv-table th {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
:is(.view-kv, [class*='-view-kv']:not([class*='-grid']), .pl-plan-kv) {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
@@ -1393,7 +1511,7 @@ body {
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-size: 13px !important;
|
||||
font-weight: 400 !important;
|
||||
color: var(--mint-text-muted) !important;
|
||||
line-height: 1.4;
|
||||
@@ -1409,7 +1527,7 @@ body {
|
||||
}
|
||||
|
||||
.view-field-label {
|
||||
font-size: 13px;
|
||||
font-size: 13px !important;
|
||||
font-weight: 400 !important;
|
||||
color: var(--mint-text-muted) !important;
|
||||
line-height: 1.4;
|
||||
@@ -1427,8 +1545,39 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* CRM 新增/编辑弹窗:label 左、控件右,垂直居中,label 可换行 */
|
||||
.crm-dialog-form {
|
||||
.el-form-item {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 栅格内纵向间距由 gap 承担,避免与 margin 叠加 */
|
||||
[class*='form-grid-'] > .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 左右布局单行更矮,适当加大栅格行间距 */
|
||||
[class*='form-grid-'] {
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
height: auto !important;
|
||||
padding-right: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog.dialog-form-layout {
|
||||
.el-form-item__label {
|
||||
font-size: 13px !important;
|
||||
color: var(--mint-text) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
@@ -1451,10 +1600,14 @@ body {
|
||||
|
||||
/* 弹窗 / 查看态内嵌列表面板(CRM 详情 Tab、表单弹窗、选择器等) */
|
||||
:is(.el-dialog, .system-view-dialog) {
|
||||
.list-body.list-wrap {
|
||||
border-top: 1px solid var(--mint-border);
|
||||
}
|
||||
|
||||
.dialog-list-panel,
|
||||
.customer-tab-list-panel {
|
||||
> .list-toolbar:first-child {
|
||||
border-radius: var(--mint-radius) var(--mint-radius) 0 0;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -1488,19 +1641,14 @@ body {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.list-body {
|
||||
border-top: 1px solid var(--mint-border);
|
||||
}
|
||||
|
||||
> .list-body.list-wrap {
|
||||
overflow: hidden;
|
||||
border-radius: 0 0 var(--mint-radius) var(--mint-radius);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
> .list-body.list-wrap.dialog-list-body-only,
|
||||
> .list-body.list-wrap.customer-tab-list-body-only {
|
||||
border-top: none;
|
||||
border-radius: var(--mint-radius);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.pagination.list-pager-bar {
|
||||
@@ -1544,6 +1692,12 @@ body {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 区块标题 / 筛选行后紧跟、未包 list-body 的裸 list-table:表头顶部分隔线 */
|
||||
:is(.dialog-section-title, .dlg-view-section-title) + .list-table.el-table thead th.el-table__cell,
|
||||
:is(.dialog-section-title, .dlg-view-section-title) + .inline-row + .list-table.el-table thead th.el-table__cell {
|
||||
border-top: 1px solid var(--mint-border) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 筛选区 label:13px 次要色(对齐 zkxw filter-form 实践;filter-hint 仍为 11px) */
|
||||
|
||||
@@ -58,6 +58,11 @@
|
||||
padding: 0 0 4px !important;
|
||||
}
|
||||
|
||||
// 弹窗/抽屉内表单 label(含 label-right):统一 13px,避免 EP 默认 14px
|
||||
:is(.el-dialog, .el-drawer) .el-form-item__label {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
// 日期选择器
|
||||
.el-date-range-picker {
|
||||
--el-datepicker-inrange-bg-color: var(--art-gray-200) !important;
|
||||
@@ -149,12 +154,17 @@ html.dark .el-card {
|
||||
border-radius: var(--mint-radius) !important;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.12) !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: var(--dialog-max-height, calc(100vh - 40px));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 16px 22px 14px !important;
|
||||
border-bottom: 1px solid var(--mint-border);
|
||||
background: linear-gradient(135deg, var(--mint-primary-bg) 0%, var(--mint-surface) 60%);
|
||||
flex-shrink: 0;
|
||||
|
||||
.el-dialog__title {
|
||||
font-size: 16px;
|
||||
@@ -163,15 +173,24 @@ html.dark .el-card {
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 18px 22px !important;
|
||||
position: relative; // 为了兼容 el-pagination 样式,需要设置 relative,不然会影响 el-pagination 的样式,比如 el-pagination__jump--small 会被影响,导致 el-pagination__jump--small 按钮无法点击,详见 URL_ADDRESS.com/element-plus/element-plus/issues/5684#issuecomment-1176299275;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
padding: 12px 22px !important;
|
||||
border-top: 1px solid var(--mint-border);
|
||||
background: var(--mint-table-head);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-dialog.el-dialog-border {
|
||||
@@ -372,8 +391,8 @@ html.dark .el-card {
|
||||
// 修改 el-message-box 样式
|
||||
.el-message-box__headerbtn .el-message-box__close,
|
||||
.el-dialog__headerbtn .el-dialog__close {
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
@@ -747,6 +766,56 @@ body .el-table thead {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 主列表(含多选列):行可单击切换选中
|
||||
.page-container .list-table.el-table:has(.el-table-column--selection) tbody tr.el-table__row,
|
||||
.console-container .list-table.el-table:has(.el-table-column--selection) tbody tr.el-table__row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// 主列表:无顶/底外框;左右外缘 + 列间竖线 + 行间横线
|
||||
.page-container .list-table.el-table,
|
||||
.console-container .list-table.el-table,
|
||||
.el-dialog .list-table.el-table,
|
||||
.system-view-dialog .list-table.el-table {
|
||||
border: none !important;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.el-table__inner-wrapper::before,
|
||||
.el-table__inner-wrapper::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.el-table__border-left-patch,
|
||||
.el-table__border-right-patch,
|
||||
.el-table__border-bottom-patch {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
th.el-table__cell,
|
||||
td.el-table__cell {
|
||||
border-right: 1px solid var(--mint-border) !important;
|
||||
border-bottom: 1px solid var(--mint-border) !important;
|
||||
border-left: none !important;
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
th.el-table__cell:first-child,
|
||||
td.el-table__cell:first-child {
|
||||
border-left: 1px solid var(--mint-border) !important;
|
||||
}
|
||||
|
||||
.el-table__fixed-right {
|
||||
th.el-table__cell:last-child,
|
||||
td.el-table__cell:last-child {
|
||||
border-right: 1px solid var(--mint-border) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-container .el-table th.el-table__cell,
|
||||
.console-container .el-table th.el-table__cell {
|
||||
color: var(--mint-text-muted);
|
||||
@@ -1196,12 +1265,24 @@ textarea::placeholder {
|
||||
color: #147a3c !important;
|
||||
}
|
||||
|
||||
// 列表分割线统一:每个分区只保留一条线
|
||||
// 工具栏与列表之间:保留 .list-body 顶部线,去掉 .list-toolbar 底部线
|
||||
// 列表分割线统一:工具栏与列表之间保留 list-body 顶部分隔线
|
||||
.page-container .list-toolbar,
|
||||
.console-container .list-toolbar,
|
||||
.el-dialog .list-toolbar {
|
||||
border-bottom: none !important;
|
||||
background: var(--mint-table-head) !important;
|
||||
}
|
||||
|
||||
.page-container .list-toolbar-row,
|
||||
.console-container .list-toolbar-row,
|
||||
.el-dialog .list-toolbar-row {
|
||||
background: var(--mint-table-head);
|
||||
}
|
||||
|
||||
.page-container .list-body,
|
||||
.console-container .list-body,
|
||||
.el-dialog .list-body {
|
||||
border-top: 1px solid var(--mint-border);
|
||||
}
|
||||
|
||||
// 列表与分页之间:保留列表底部线,去掉分页顶部线;分页控件右对齐(§7.9)
|
||||
|
||||
@@ -48,4 +48,8 @@
|
||||
--danger-bg: #fef2f2;
|
||||
--info: #64748b;
|
||||
--info-bg: #f8fafc;
|
||||
|
||||
/* 弹窗相对视口上下留白(各 20px,总 40px) */
|
||||
--dialog-viewport-inset: 20px;
|
||||
--dialog-max-height: calc(100vh - var(--dialog-viewport-inset) * 2);
|
||||
}
|
||||
@@ -33,3 +33,12 @@
|
||||
|
||||
// 地磅(db)模块 Fresh Mint 公共样式
|
||||
@use '../../styles/db-module.scss';
|
||||
|
||||
// 运输(pl)模块样式
|
||||
@use '../../styles/pl-module.scss';
|
||||
|
||||
// 工作流(wf)模块样式
|
||||
@use '../../styles/wf-module.scss';
|
||||
|
||||
// 系统管理(sys)模块样式
|
||||
@use '../../styles/sys-module.scss';
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="attachment-upload">
|
||||
<div
|
||||
class="attachment-upload"
|
||||
:class="{
|
||||
'attachment-upload--compact': compact,
|
||||
'attachment-upload--readonly': compact && disabled
|
||||
}"
|
||||
>
|
||||
<div class="attachment-list">
|
||||
<el-upload
|
||||
v-if="!disabled"
|
||||
@@ -11,7 +17,7 @@
|
||||
@change="handleAttachmentChange"
|
||||
>
|
||||
<div class="upload-card">
|
||||
<el-icon :size="28" color="#1ca851"><Plus /></el-icon>
|
||||
<el-icon :size="previewIconSize" color="#1ca851"><Plus /></el-icon>
|
||||
<span class="upload-card-text">上传附件</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
@@ -23,18 +29,18 @@
|
||||
>
|
||||
<div
|
||||
class="attachment-preview"
|
||||
:class="{ 'is-clickable': canPreview(file) }"
|
||||
@click="canPreview(file) ? handlePreviewAttachment(file) : undefined"
|
||||
:class="{ 'is-clickable': Boolean(file.fileUrl) }"
|
||||
@click="file.fileUrl ? handleAttachmentClick(file) : undefined"
|
||||
>
|
||||
<template v-if="isImageFile(file) && file.fileUrl">
|
||||
<img :src="resolveFileUrl(file.fileUrl)" :alt="file.fileName" />
|
||||
</template>
|
||||
<template v-else-if="isImageFile(file) && !file.fileUrl">
|
||||
<el-icon :size="24" color="#94a3b8"><Loading /></el-icon>
|
||||
<el-icon :size="previewIconSize" color="#94a3b8"><Loading /></el-icon>
|
||||
</template>
|
||||
<template v-else-if="isVideoFile(file)">
|
||||
<div class="attachment-preview__icon attachment-preview__icon--video">
|
||||
<el-icon :size="28"><VideoPlay /></el-icon>
|
||||
<el-icon :size="previewIconSize"><VideoPlay /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -42,13 +48,22 @@
|
||||
class="attachment-preview__icon"
|
||||
:class="isPdfFile(file) ? 'attachment-preview__icon--pdf' : 'attachment-preview__icon--doc'"
|
||||
>
|
||||
<el-icon :size="28"><Document /></el-icon>
|
||||
<el-icon :size="previewIconSize"><Document /></el-icon>
|
||||
<span v-if="getFileExtension(file.fileName)" class="attachment-preview__ext">
|
||||
{{ getFileExtension(file.fileName) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<button
|
||||
v-if="compact && !disabled"
|
||||
type="button"
|
||||
class="attachment-compact-delete"
|
||||
title="删除"
|
||||
@click.stop="removeAttachment(index)"
|
||||
>
|
||||
<el-icon :size="12"><Delete /></el-icon>
|
||||
</button>
|
||||
<div class="attachment-footer">
|
||||
<span class="attachment-name" :title="file.fileName">{{ file.fileName }}</span>
|
||||
<div v-if="!disabled" class="attachment-actions">
|
||||
@@ -71,7 +86,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="!disabled" class="upload-hint">{{ hint }}</span>
|
||||
<span v-if="!disabled && !compact" class="upload-hint">{{ hint }}</span>
|
||||
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
@@ -148,6 +163,8 @@ const props = withDefaults(
|
||||
accept?: string
|
||||
hint?: string
|
||||
disabled?: boolean
|
||||
/** 紧凑模式:小缩略图,只读时隐藏文件名与操作栏 */
|
||||
compact?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
@@ -155,7 +172,8 @@ const props = withDefaults(
|
||||
limit: 15,
|
||||
accept: 'image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar',
|
||||
hint: '支持图片、视频、PDF、Word、Excel等文件',
|
||||
disabled: false
|
||||
disabled: false,
|
||||
compact: false
|
||||
}
|
||||
)
|
||||
|
||||
@@ -170,6 +188,11 @@ const previewFile = ref<AttachmentItem | null>(null)
|
||||
const previewPdfSrc = ref('')
|
||||
const previewLoading = ref(false)
|
||||
|
||||
const previewIconSize = computed(() => {
|
||||
if (!props.compact) return 28
|
||||
return props.disabled ? 14 : 20
|
||||
})
|
||||
|
||||
function resolveFileUrl(url: string) {
|
||||
const trimmed = String(url || '').trim()
|
||||
if (!trimmed) return ''
|
||||
@@ -325,6 +348,18 @@ function handlePreviewAttachment(file: AttachmentItem) {
|
||||
})
|
||||
}
|
||||
|
||||
function handleAttachmentClick(file: AttachmentItem) {
|
||||
if (!file.fileUrl) return
|
||||
if (canPreview(file)) {
|
||||
handlePreviewAttachment(file)
|
||||
return
|
||||
}
|
||||
const url = resolveFileUrl(file.fileUrl)
|
||||
if (url) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
}
|
||||
|
||||
function handlePreviewClosed() {
|
||||
previewFile.value = null
|
||||
previewPdfSrc.value = ''
|
||||
@@ -532,4 +567,108 @@ defineExpose({ hasUploading })
|
||||
:deep(.el-upload) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.attachment-upload--compact {
|
||||
.attachment-list {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.attachment-preview__ext {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.attachment-footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格编辑:中等缩略图,支持预览 + 角标删除 */
|
||||
.attachment-upload--compact:not(.attachment-upload--readonly) {
|
||||
.upload-card {
|
||||
width: 88px;
|
||||
height: 66px;
|
||||
gap: 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
position: relative;
|
||||
width: 88px;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 6px rgba(28, 168, 81, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
height: 66px;
|
||||
}
|
||||
|
||||
.attachment-preview__icon {
|
||||
width: 36px;
|
||||
min-height: 36px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.attachment-compact-delete {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.72);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #ef4444;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 只读紧凑:更小缩略图(查看弹窗等) */
|
||||
.attachment-upload--compact.attachment-upload--readonly {
|
||||
.attachment-list {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
gap: 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 40px;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 1px 4px rgba(28, 168, 81, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-preview {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.attachment-preview__icon {
|
||||
width: 24px;
|
||||
min-height: 24px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!-- 列表工具栏多功能检索框(漏斗切换由 filter-collapse 注入到 .quick-search-group) -->
|
||||
<template>
|
||||
<el-input
|
||||
:model-value="modelValue"
|
||||
class="quick-search-input"
|
||||
:placeholder="placeholder"
|
||||
clearable
|
||||
@update:model-value="emit('update:modelValue', $event ?? '')"
|
||||
@keyup.enter="emit('search')"
|
||||
@clear="emit('clear')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="quick-search-icon" @click="emit('search')"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({ name: 'ListQuickSearchInput' })
|
||||
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
placeholder: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
search: []
|
||||
clear: []
|
||||
}>()
|
||||
</script>
|
||||
@@ -1,90 +1,84 @@
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="innerVisible"
|
||||
<SystemViewDialog
|
||||
v-model:visible="innerVisible"
|
||||
:title="carrier?.carrierName || '承运商详情'"
|
||||
width="1080px"
|
||||
align-center
|
||||
destroy-on-close
|
||||
class="dialog-form-layout carrier-view-dialog"
|
||||
:icon="OfficeBuilding"
|
||||
:meta="heroMeta"
|
||||
:sections="viewSections"
|
||||
:loading="loading"
|
||||
width="1400px"
|
||||
dialog-class="pl-carrier-view-dialog"
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
style="backdrop-filter: none"
|
||||
@closed="handleClosed"
|
||||
@update:visible="handleVisibleChange"
|
||||
>
|
||||
<div class="carrier-view-body" v-loading="loading">
|
||||
<div v-if="$slots['hero-actions']" class="carrier-view-hero-actions">
|
||||
<slot name="hero-actions" />
|
||||
</div>
|
||||
<section class="carrier-view-top">
|
||||
<h3 class="carrier-view-block-title">承运商信息</h3>
|
||||
<ViewKvTable v-if="carrier" :fields="carrierViewFields" mode="triple" />
|
||||
<template #hero-actions>
|
||||
<slot name="hero-actions" />
|
||||
</template>
|
||||
|
||||
<h3 class="carrier-view-block-title carrier-view-attachment-title">资质附件</h3>
|
||||
<div v-if="carrier" class="carrier-view-attachments form-grid-2">
|
||||
<div class="carrier-view-attachment-item">
|
||||
<div class="carrier-view-attachment-label">营业执照</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.businessLicenseUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/license"
|
||||
/>
|
||||
</div>
|
||||
<div class="carrier-view-attachment-item">
|
||||
<div class="carrier-view-attachment-label">道路经营许可证</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.roadTransportLicenseUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/permit"
|
||||
/>
|
||||
</div>
|
||||
<div class="carrier-view-attachment-item form-col-span-2">
|
||||
<div class="carrier-view-attachment-label">运输协议</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.transportAgreementUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/agreement"
|
||||
/>
|
||||
</div>
|
||||
<template #carrierAttachments>
|
||||
<div v-if="carrier" class="carrier-view-attachments form-grid-3">
|
||||
<div class="carrier-view-attachment-item">
|
||||
<div class="carrier-view-attachment-label">营业执照</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.businessLicenseUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/license"
|
||||
/>
|
||||
</div>
|
||||
<div class="carrier-view-attachment-item">
|
||||
<div class="carrier-view-attachment-label">运输协议</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.transportAgreementUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/agreement"
|
||||
/>
|
||||
</div>
|
||||
<div class="carrier-view-attachment-item">
|
||||
<div class="carrier-view-attachment-label">道路经营许可证</div>
|
||||
<AttachmentUploadField
|
||||
:model-value="carrier.roadTransportLicenseUrls || ''"
|
||||
disabled
|
||||
folder="pl/carrier/permit"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="carrier-view-bottom">
|
||||
<h3 class="carrier-view-block-title">关联车辆</h3>
|
||||
<p v-if="!vehicleBundles.length && !loading" class="carrier-view-empty">暂无关联车辆</p>
|
||||
<el-tabs v-else v-model="activeTab" class="carrier-vehicle-tabs">
|
||||
<el-tab-pane
|
||||
v-for="bundle in vehicleBundles"
|
||||
:key="bundle.detail.id"
|
||||
:label="bundle.detail.licensePlate"
|
||||
:name="bundle.detail.id"
|
||||
>
|
||||
<PlVehicleViewPanel
|
||||
:detail="bundle.detail"
|
||||
:personnel-list="bundle.personnelList"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="innerVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<template #carrierVehicles>
|
||||
<p v-if="!vehicleBundles.length && !loading" class="carrier-view-empty">暂无关联车辆</p>
|
||||
<el-tabs v-else v-model="activeTab" class="carrier-vehicle-tabs">
|
||||
<el-tab-pane
|
||||
v-for="bundle in vehicleBundles"
|
||||
:key="bundle.detail.id"
|
||||
:label="bundle.detail.licensePlate"
|
||||
:name="bundle.detail.id"
|
||||
lazy
|
||||
>
|
||||
<PlVehicleViewPanel
|
||||
v-if="activeTab === bundle.detail.id"
|
||||
:key="`${viewSessionKey}-${bundle.detail.id}`"
|
||||
:detail="bundle.detail"
|
||||
:personnel-list="bundle.personnelList"
|
||||
table-mode="quint"
|
||||
cert-layout="table"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import { OfficeBuilding } from '@element-plus/icons-vue'
|
||||
import { carrierApi, vehicleApi, type Carrier } from '@/api/pl'
|
||||
import { pickBxList } from '@/utils/http/bx'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlVehicleViewPanel from '@/components/business/pl-vehicle-view-panel/index.vue'
|
||||
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
|
||||
import SystemViewDialog, {
|
||||
type ViewSection
|
||||
} from '@/components/business/system-view-dialog/index.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import {
|
||||
loadVehicleViewBundles,
|
||||
@@ -116,44 +110,130 @@ const loading = ref(false)
|
||||
const carrier = ref<Carrier | null>(null)
|
||||
const vehicleBundles = ref<VehicleViewBundle[]>([])
|
||||
const activeTab = ref('')
|
||||
const viewSessionKey = ref(0)
|
||||
|
||||
const carrierViewFields = computed<ViewField[]>(() => {
|
||||
const heroMeta = computed(() => {
|
||||
const c = carrier.value
|
||||
if (!c) return ''
|
||||
const parts = [
|
||||
c.contactName || undefined,
|
||||
c.contactPhone || undefined,
|
||||
getPlCarrierStatusLabel(c.status),
|
||||
getPlCarrierAuditStatusLabel(c.auditStatus)
|
||||
].filter(Boolean)
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
const carrierInfoRow1Fields = computed<ViewField[]>(() => {
|
||||
const c = carrier.value
|
||||
if (!c) return []
|
||||
|
||||
return [
|
||||
{ label: '承运商名称', value: c.carrierName },
|
||||
{ label: '联系人', value: c.contactName || '—' },
|
||||
{ label: '联系电话', value: c.contactPhone || '—' },
|
||||
{ label: '承运商名称', value: c.carrierName, long: false },
|
||||
{ label: '联系人', value: c.contactName || '—', long: false },
|
||||
{ label: '联系电话', value: c.contactPhone || '—', long: false },
|
||||
{
|
||||
label: '合作状态',
|
||||
value: getPlCarrierStatusLabel(c.status),
|
||||
tagType: getPlCarrierStatusTagType(c.status)
|
||||
},
|
||||
tagType: getPlCarrierStatusTagType(c.status),
|
||||
long: false
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const carrierInfoRow2Fields = computed<ViewField[]>(() => {
|
||||
const c = carrier.value
|
||||
if (!c) return []
|
||||
|
||||
return [
|
||||
{
|
||||
label: '审核状态',
|
||||
value: getPlCarrierAuditStatusLabel(c.auditStatus),
|
||||
tagType: getPlCarrierAuditStatusTagType(c.auditStatus)
|
||||
tagType: getPlCarrierAuditStatusTagType(c.auditStatus),
|
||||
long: false
|
||||
},
|
||||
{
|
||||
label: '审核时间',
|
||||
value: c.auditTime ? formatDateTime(c.auditTime) : '—'
|
||||
value: c.auditTime ? formatDateTime(c.auditTime) : '—',
|
||||
long: false
|
||||
},
|
||||
{ label: '审核备注', value: c.auditRemark || '—', long: false, fullSpan: false }
|
||||
]
|
||||
})
|
||||
|
||||
const carrierInfoRow3Fields = computed<ViewField[]>(() => {
|
||||
const c = carrier.value
|
||||
if (!c) return []
|
||||
|
||||
return [
|
||||
{ label: '创建时间', value: formatDateTime(c.createTime), long: false },
|
||||
{
|
||||
label: '协议有效期',
|
||||
value: c.transportAgreementExpired
|
||||
? '协议已过期'
|
||||
: c.transportAgreementExpireDate || '—',
|
||||
tagType: c.transportAgreementExpired ? 'danger' : undefined
|
||||
tagType: c.transportAgreementExpired ? 'danger' : undefined,
|
||||
long: false
|
||||
},
|
||||
{
|
||||
label: '资质状态',
|
||||
value: c.qualificationComplete ? '已齐全' : '资质不全',
|
||||
tagType: c.qualificationComplete ? 'success' : 'warning'
|
||||
tagType: c.qualificationComplete ? 'success' : 'warning',
|
||||
long: false
|
||||
},
|
||||
{ label: '创建时间', value: formatDateTime(c.createTime) },
|
||||
{ label: '审核备注', value: c.auditRemark || '—', span: 'full' },
|
||||
{ label: '备注', value: c.remark || '—', span: 'full' }
|
||||
{ label: '备注', value: c.remark || '—', long: false, fullSpan: false }
|
||||
]
|
||||
})
|
||||
|
||||
const viewSections = computed<ViewSection[]>(() => {
|
||||
if (!carrier.value) {
|
||||
return [
|
||||
{
|
||||
title: '承运商信息',
|
||||
fieldGroups: [
|
||||
{ fieldLayout: 'table', tableMode: 'quad', fields: [] },
|
||||
{ fieldLayout: 'table', tableMode: 'quad', fields: [] },
|
||||
{ fieldLayout: 'table', tableMode: 'triple', fields: [] }
|
||||
],
|
||||
fields: []
|
||||
},
|
||||
{ title: '资质附件', fields: [], slot: 'carrierAttachments' },
|
||||
{ title: '关联车辆', fields: [], slot: 'carrierVehicles' }
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: '承运商信息',
|
||||
fieldGroups: [
|
||||
{
|
||||
fieldLayout: 'table',
|
||||
tableMode: 'quad',
|
||||
fields: carrierInfoRow1Fields.value
|
||||
},
|
||||
{
|
||||
fieldLayout: 'table',
|
||||
tableMode: 'quad',
|
||||
fields: carrierInfoRow3Fields.value
|
||||
},
|
||||
{
|
||||
fieldLayout: 'table',
|
||||
tableMode: 'triple',
|
||||
fields: carrierInfoRow2Fields.value
|
||||
}
|
||||
],
|
||||
fields: []
|
||||
},
|
||||
{
|
||||
title: '资质附件',
|
||||
fields: [],
|
||||
slot: 'carrierAttachments'
|
||||
},
|
||||
{
|
||||
title: '关联车辆',
|
||||
fields: [],
|
||||
slot: 'carrierVehicles'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -171,6 +251,7 @@ async function loadDetail(carrierId: string) {
|
||||
carrier.value = null
|
||||
vehicleBundles.value = []
|
||||
activeTab.value = ''
|
||||
viewSessionKey.value += 1
|
||||
try {
|
||||
const [carrierRes, vehicleListRes] = await Promise.all([
|
||||
carrierApi.get(carrierId),
|
||||
@@ -188,47 +269,24 @@ async function loadDetail(carrierId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleClosed() {
|
||||
carrier.value = null
|
||||
vehicleBundles.value = []
|
||||
activeTab.value = ''
|
||||
function handleVisibleChange(value: boolean) {
|
||||
if (!value) {
|
||||
carrier.value = null
|
||||
vehicleBundles.value = []
|
||||
activeTab.value = ''
|
||||
viewSessionKey.value += 1
|
||||
}
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
if (!props.visible || !props.carrierId) return
|
||||
await loadDetail(props.carrierId)
|
||||
}
|
||||
|
||||
defineExpose({ reload })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.carrier-view-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.carrier-view-hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.carrier-view-block-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.carrier-view-top {
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.carrier-view-attachment-title {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.carrier-view-attachments {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
@@ -238,13 +296,13 @@ function handleClosed() {
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.carrier-view-empty {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.carrier-vehicle-tabs {
|
||||
@@ -257,3 +315,27 @@ function handleClosed() {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.pl-carrier-view-dialog .view-section:first-of-type .view-field-group--spaced {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pl-carrier-view-dialog .view-section:first-of-type .view-field-group .view-kv-table-wrap {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.pl-carrier-view-dialog .view-section:first-of-type .view-field-group:first-child .view-kv-table-wrap {
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
.pl-carrier-view-dialog .view-section:first-of-type .view-field-group--spaced .view-kv-table-wrap {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.pl-carrier-view-dialog .view-section:first-of-type .view-field-group:last-child .view-kv-table-wrap {
|
||||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,92 @@
|
||||
|
||||
<div v-if="!certList.length" class="cert-empty">暂无证书,点击「添加证书」</div>
|
||||
|
||||
<el-table v-else-if="layout === 'table'" :data="certList" stripe class="list-table cert-form-table">
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
v-if="showVehiclePartColumn"
|
||||
label="车牌号"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-if="hasTrailerPlateOption"
|
||||
:model-value="normalizeVehicleCertPart(row.vehiclePart)"
|
||||
placeholder="请选择车牌号"
|
||||
style="width: 100%"
|
||||
@update:model-value="handleVehiclePartChange(row, $event)"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in vehiclePlateOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
<span v-else>{{ headPlateLabel }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="证书名称" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-select
|
||||
v-model="row.certName"
|
||||
placeholder="请选择或输入"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
>
|
||||
<el-option v-for="name in certNameOptions" :key="name" :label="name" :value="name" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="证号" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.certNo" placeholder="请输入证号" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="有效期" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-date-picker
|
||||
v-model="row.expireDate"
|
||||
type="date"
|
||||
placeholder="选择有效期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提醒天数" min-width="110" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.remindDays"
|
||||
:min="1"
|
||||
:max="365"
|
||||
:controls="false"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" min-width="220">
|
||||
<template #default="{ row, $index }">
|
||||
<AttachmentUploadField
|
||||
:ref="(el) => setCertUploadRef($index, el)"
|
||||
v-model="row.attachmentUrls"
|
||||
:folder="uploadFolder"
|
||||
compact
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="60" align="center" header-align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="handleRemoveCert($index)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div v-else class="cert-card-list">
|
||||
<div v-for="(cert, index) in certList" :key="cert._key" class="cert-card">
|
||||
<div class="cert-card-head">
|
||||
@@ -43,9 +129,6 @@
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="提醒天数">
|
||||
<template #label>
|
||||
提醒天数<span class="form-label-tip">默认 30 天</span>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="cert.remindDays"
|
||||
:min="1"
|
||||
@@ -71,6 +154,11 @@
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import type { CertRow } from '@/api/pl'
|
||||
import {
|
||||
PL_VEHICLE_CERT_PART_HEAD,
|
||||
PL_VEHICLE_CERT_PART_TRAILER,
|
||||
normalizeVehicleCertPart
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
|
||||
type CertRowWithKey = CertRow & { _key: string }
|
||||
type CertUploadRef = InstanceType<typeof AttachmentUploadField>
|
||||
@@ -80,13 +168,45 @@ const props = withDefaults(
|
||||
title?: string
|
||||
uploadFolder: string
|
||||
certNameOptions?: string[]
|
||||
layout?: 'card' | 'table'
|
||||
/** 列表模式:展示车牌号列,可从主车/挂车车牌中选择 */
|
||||
showVehiclePartColumn?: boolean
|
||||
licensePlate?: string
|
||||
trailerNo?: string
|
||||
}>(),
|
||||
{
|
||||
title: '证书',
|
||||
certNameOptions: () => []
|
||||
certNameOptions: () => [],
|
||||
layout: 'card',
|
||||
showVehiclePartColumn: false,
|
||||
licensePlate: '',
|
||||
trailerNo: ''
|
||||
}
|
||||
)
|
||||
|
||||
const headPlateLabel = computed(() => props.licensePlate?.trim() || '—')
|
||||
const hasTrailerPlateOption = computed(() => Boolean(props.trailerNo?.trim()))
|
||||
|
||||
const vehiclePlateOptions = computed(() => {
|
||||
const options = [
|
||||
{
|
||||
label: headPlateLabel.value,
|
||||
value: PL_VEHICLE_CERT_PART_HEAD
|
||||
}
|
||||
]
|
||||
if (hasTrailerPlateOption.value) {
|
||||
options.push({
|
||||
label: props.trailerNo?.trim() || '挂车',
|
||||
value: PL_VEHICLE_CERT_PART_TRAILER
|
||||
})
|
||||
}
|
||||
return options
|
||||
})
|
||||
|
||||
function handleVehiclePartChange(row: CertRowWithKey, value: string) {
|
||||
row.vehiclePart = normalizeVehicleCertPart(value)
|
||||
}
|
||||
|
||||
const certList = defineModel<CertRowWithKey[]>('modelValue', { default: () => [] })
|
||||
|
||||
const certUploadRefs = ref<Record<number, CertUploadRef | null>>({})
|
||||
@@ -94,6 +214,7 @@ const certUploadRefs = ref<Record<number, CertUploadRef | null>>({})
|
||||
function createEmptyCert(): CertRowWithKey {
|
||||
return {
|
||||
_key: `cert-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
vehiclePart: PL_VEHICLE_CERT_PART_HEAD,
|
||||
certName: '',
|
||||
certNo: '',
|
||||
expireDate: '',
|
||||
@@ -161,6 +282,29 @@ defineExpose({ hasUploading, clearUploadRefs })
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.cert-form-table {
|
||||
border-top: 1px solid var(--mint-border, #d9e8e2);
|
||||
|
||||
:deep(.el-select),
|
||||
:deep(.el-input),
|
||||
:deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.attachment-upload--compact:not(.attachment-upload--readonly)) {
|
||||
min-width: 0;
|
||||
|
||||
.attachment-upload {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cert-card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -207,7 +351,7 @@ defineExpose({ hasUploading, clearUploadRefs })
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text);
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,85 @@
|
||||
<template>
|
||||
<div class="cert-view-section">
|
||||
<p v-if="!certList.length" class="view-empty-hint">暂无证书</p>
|
||||
<p v-if="!certList.length && layout === 'card'" class="view-empty-hint">暂无证书</p>
|
||||
|
||||
<div v-else-if="layout === 'table'" class="panel dialog-list-panel">
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<el-table
|
||||
:data="certList"
|
||||
class="list-table"
|
||||
stripe
|
||||
empty-text="暂无证书"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="60"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="index => index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="showVehiclePartColumn"
|
||||
label="车牌号"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ resolveVehiclePartLabel(row) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="certName"
|
||||
label="证书名称"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="certNo"
|
||||
label="证号"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.certNo || '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="有效期" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.expireDate"
|
||||
:type="getExpireTagType(row.expireDate)"
|
||||
size="small"
|
||||
>
|
||||
{{ row.expireDate }}
|
||||
</el-tag>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提醒天数" min-width="96" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ row.remindDays ?? 30 }} 天</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="附件" min-width="160" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.attachmentUrls" class="cert-view-attachment-cell">
|
||||
<AttachmentUploadField
|
||||
:model-value="row.attachmentUrls"
|
||||
disabled
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
<span v-else>—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="cert-view-list">
|
||||
<div
|
||||
@@ -11,15 +90,16 @@
|
||||
<h4 class="cert-view-card-title">证书 {{ index + 1 }}</h4>
|
||||
|
||||
<div class="cert-view-kv-table-group">
|
||||
<ViewKvTable :fields="buildCertViewFields(cert)" mode="triple" />
|
||||
<ViewKvTable :fields="buildCertViewFields(cert)" :mode="tableMode" />
|
||||
<table
|
||||
v-if="cert.attachmentUrls"
|
||||
class="cert-view-attachment-table view-kv-table view-kv-table--triple"
|
||||
class="cert-view-attachment-table view-kv-table"
|
||||
:class="`view-kv-table--${tableMode}`"
|
||||
>
|
||||
<tbody>
|
||||
<tr class="row-long">
|
||||
<th>附件</th>
|
||||
<td colspan="5">
|
||||
<td :colspan="attachmentColspan">
|
||||
<AttachmentUploadField
|
||||
:model-value="cert.attachmentUrls"
|
||||
disabled
|
||||
@@ -37,18 +117,51 @@
|
||||
<script setup lang="ts">
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import type { ViewField, ViewTableMode } from '@/components/business/system-view-dialog/types'
|
||||
import type { CertRead } from '@/api/pl'
|
||||
import {
|
||||
PL_VEHICLE_CERT_PART_TRAILER,
|
||||
normalizeVehicleCertPart
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
|
||||
withDefaults(
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
certList?: CertRead[]
|
||||
tableMode?: ViewTableMode
|
||||
/** card:卡片 + KV 表格;table:列表表格 + 小缩略图 */
|
||||
layout?: 'card' | 'table'
|
||||
/** 列表模式:有挂车时展示主车/挂车部位列 */
|
||||
showVehiclePartColumn?: boolean
|
||||
headLabel?: string
|
||||
trailerLabel?: string
|
||||
}>(),
|
||||
{
|
||||
certList: () => []
|
||||
certList: () => [],
|
||||
tableMode: 'triple',
|
||||
layout: 'card',
|
||||
showVehiclePartColumn: false,
|
||||
headLabel: '主车',
|
||||
trailerLabel: '挂车'
|
||||
}
|
||||
)
|
||||
|
||||
const attachmentColspanMap: Record<ViewTableMode, number> = {
|
||||
pair: 1,
|
||||
dual: 3,
|
||||
triple: 5,
|
||||
quad: 7,
|
||||
quint: 9
|
||||
}
|
||||
|
||||
const attachmentColspan = computed(() => attachmentColspanMap[props.tableMode])
|
||||
|
||||
function resolveVehiclePartLabel(cert: CertRead) {
|
||||
if (normalizeVehicleCertPart(cert.vehiclePart) === PL_VEHICLE_CERT_PART_TRAILER) {
|
||||
return props.trailerLabel?.trim() || '挂车'
|
||||
}
|
||||
return props.headLabel?.trim() || '主车'
|
||||
}
|
||||
|
||||
function getExpireTagType(dateStr: string | undefined) {
|
||||
if (!dateStr) return 'info'
|
||||
const expDate = new Date(dateStr)
|
||||
@@ -85,6 +198,20 @@ function buildCertViewFields(cert: CertRead): ViewField[] {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.cert-view-attachment-cell {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
:deep(.attachment-upload) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
:deep(.attachment-list) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.cert-view-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -129,7 +256,6 @@ function buildCertViewFields(cert: CertRead): ViewField[] {
|
||||
border-top: 1px solid #eef2f6;
|
||||
|
||||
th {
|
||||
width: 10%;
|
||||
padding: 10px 12px;
|
||||
background: var(--table-head, #f8fafc);
|
||||
color: var(--text-muted, #64748b);
|
||||
@@ -152,4 +278,12 @@ function buildCertViewFields(cert: CertRead): ViewField[] {
|
||||
line-height: 1.45;
|
||||
}
|
||||
}
|
||||
|
||||
.cert-view-attachment-table.view-kv-table--triple th {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.cert-view-attachment-table.view-kv-table--quint th {
|
||||
width: 6.5%;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="640px"
|
||||
width="740px"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@@ -14,7 +14,9 @@
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
label-position="right"
|
||||
label-width="100px"
|
||||
class="pl-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
@@ -31,9 +33,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="actionType === 'submit'" prop="approverIds" class="form-col-span-2">
|
||||
<template #label>
|
||||
选择审批人<span class="form-label-tip">支持搜索用户姓名</span>
|
||||
</template>
|
||||
<template #label>选择审批人</template>
|
||||
<el-select
|
||||
v-model="form.approverIds"
|
||||
multiple
|
||||
@@ -71,9 +71,7 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="actionType === 'submit'" prop="remark" class="form-col-span-2">
|
||||
<template #label>
|
||||
提交说明<span class="form-label-tip">可选</span>
|
||||
</template>
|
||||
<template #label>提交说明</template>
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
|
||||
@@ -60,7 +60,9 @@
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
label-position="right"
|
||||
label-width="100px"
|
||||
class="pl-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
v-loading="loading"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
label-position="right"
|
||||
label-width="100px"
|
||||
class="pl-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
|
||||
@@ -30,30 +30,39 @@
|
||||
|
||||
<section v-if="settlement" class="view-section">
|
||||
<h4 class="view-section-title">发票信息</h4>
|
||||
<div class="view-kv-grid view-kv-grid--cols-3">
|
||||
<div class="view-kv-item">
|
||||
<span class="view-kv-label">发票号</span>
|
||||
<span class="view-kv-value">{{ settlement.invoiceNo || '—' }}</span>
|
||||
<div class="panel dialog-list-panel">
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<el-table
|
||||
:data="invoiceRows"
|
||||
class="list-table"
|
||||
stripe
|
||||
empty-text="暂无数据"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" />
|
||||
<el-table-column prop="invoiceNo" label="发票号" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.invoiceNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="invoiceDate" label="开票日期" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.invoiceDate || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="invoiceAmount" label="发票金额(元)" min-width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ row.invoiceAmount != null ? formatAmount(row.invoiceAmount) : '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发票附件" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.invoiceAttachmentUrls" class="invoice-attachment-half">
|
||||
<AttachmentUploadField
|
||||
:model-value="row.invoiceAttachmentUrls"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
<span v-else class="view-kv-value">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="view-kv-item">
|
||||
<span class="view-kv-label">开票日期</span>
|
||||
<span class="view-kv-value">{{ settlement.invoiceDate || '—' }}</span>
|
||||
</div>
|
||||
<div class="view-kv-item">
|
||||
<span class="view-kv-label">发票金额</span>
|
||||
<span class="view-kv-value">{{
|
||||
settlement.invoiceAmount ? formatAmount(settlement.invoiceAmount) : '—'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-kv-item view-kv-item--full">
|
||||
<span class="view-kv-label">发票附件</span>
|
||||
<AttachmentUploadField
|
||||
v-if="settlement.invoiceAttachmentUrls"
|
||||
:model-value="settlement.invoiceAttachmentUrls"
|
||||
disabled
|
||||
/>
|
||||
<span v-else class="view-kv-value">—</span>
|
||||
</div>
|
||||
<div class="view-kv-grid view-kv-grid--cols-2 view-kv-grid--mt">
|
||||
<div class="view-kv-item">
|
||||
@@ -120,9 +129,16 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="settlement?.approvalFlow?.length" class="view-section">
|
||||
<section
|
||||
v-if="settlement && (settlement.approvalFlow?.length || settlement.approvalHistory?.length)"
|
||||
class="view-section"
|
||||
>
|
||||
<h4 class="view-section-title">审批流程</h4>
|
||||
<ApprovalFlowView :nodes="approvalNodes" :loading="false" />
|
||||
<BxApprovalFlowTabs
|
||||
:nodes="approvalNodes"
|
||||
:history="approvalHistoryItems"
|
||||
:loading="false"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section
|
||||
@@ -170,18 +186,12 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close } from '@element-plus/icons-vue'
|
||||
import { freightApi, type FreightSettlement } from '@/api/pl'
|
||||
import ApprovalFlowView from '@/components/bx/base/ApprovalFlowView.vue'
|
||||
import BxApprovalFlowTabs from '@/components/bx/base/BxApprovalFlowTabs.vue'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import { normalizeAttachmentUrls } from '@/utils/download-attachment'
|
||||
import {
|
||||
@@ -201,6 +211,29 @@ const approvalNodes = computed(() =>
|
||||
mapApprovalFlowToNodes(settlement.value?.approvalFlow || [])
|
||||
)
|
||||
|
||||
const approvalHistoryItems = computed(() =>
|
||||
mapApprovalFlowToNodes(settlement.value?.approvalHistory || [])
|
||||
)
|
||||
|
||||
const invoiceRows = computed(() => {
|
||||
const current = settlement.value
|
||||
if (!current) return []
|
||||
const hasInvoice =
|
||||
Boolean(current.invoiceNo?.trim()) ||
|
||||
Boolean(current.invoiceDate) ||
|
||||
(current.invoiceAmount != null && Number(current.invoiceAmount) > 0) ||
|
||||
Boolean(current.invoiceAttachmentUrls?.trim())
|
||||
if (!hasInvoice) return []
|
||||
return [
|
||||
{
|
||||
invoiceNo: current.invoiceNo,
|
||||
invoiceDate: current.invoiceDate,
|
||||
invoiceAmount: current.invoiceAmount,
|
||||
invoiceAttachmentUrls: current.invoiceAttachmentUrls
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
function formatAmount(amount?: number) {
|
||||
if (!amount) return '¥0.00'
|
||||
return formatMoneyDisplay(amount)
|
||||
@@ -357,6 +390,53 @@ defineExpose({ open, reload })
|
||||
}
|
||||
}
|
||||
|
||||
.invoice-attachment-half {
|
||||
:deep(.attachment-list) {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
:deep(.attachment-item) {
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
:deep(.attachment-preview) {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
:deep(.attachment-preview__icon) {
|
||||
width: 24px;
|
||||
min-height: 24px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.attachment-preview__ext) {
|
||||
margin-top: 2px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
:deep(.attachment-footer) {
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
:deep(.attachment-name) {
|
||||
font-size: 9px;
|
||||
-webkit-line-clamp: 1;
|
||||
}
|
||||
|
||||
:deep(.attachment-actions) {
|
||||
gap: 4px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
:deep(.action-btn) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.view-kv-grid--cols-3 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<section class="pl-arrange-form-panel">
|
||||
<div v-loading="loading" class="pl-arrange-form-scroll">
|
||||
<section class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">产品实际数量</h3>
|
||||
<h4 class="pl-plan-subtitle">产品实际数量</h4>
|
||||
<PlPlanFormProductTable
|
||||
:products="productRows"
|
||||
show-actual-qty
|
||||
@@ -29,7 +29,7 @@
|
||||
<h4 class="pl-plan-subtitle">运费信息</h4>
|
||||
<el-form
|
||||
:model="formData"
|
||||
label-width="96px"
|
||||
label-width="140px"
|
||||
label-position="right"
|
||||
class="pl-plan-freight-form"
|
||||
>
|
||||
@@ -119,6 +119,7 @@ import {
|
||||
import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
|
||||
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { enrichPlanProductsHazardFromCrm } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { usePlFreightCalc, resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
@@ -258,7 +259,9 @@ async function handleOpened() {
|
||||
throw new Error('运输计划数据无效')
|
||||
}
|
||||
plan.value = data
|
||||
productRows.value = mapPlanProducts(Array.isArray(data.products) ? data.products : [])
|
||||
const rawProducts = Array.isArray(data.products) ? data.products : []
|
||||
const enrichedProducts = await enrichPlanProductsHazardFromCrm(rawProducts)
|
||||
productRows.value = mapPlanProducts(enrichedProducts)
|
||||
await applyFreightPrefill(data)
|
||||
} catch (error) {
|
||||
innerVisible.value = false
|
||||
@@ -356,6 +359,9 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
.pl-arrange-form-scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
max-height: 52vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
@@ -377,14 +383,21 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
.pl-plan-subtitle {
|
||||
margin: 16px 0 10px;
|
||||
margin: 0 0 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.pl-plan-subtitle:first-of-type {
|
||||
margin-top: 0;
|
||||
.pl-plan-freight-section {
|
||||
.pl-plan-subtitle {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
width: 140px !important;
|
||||
max-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-freight-form {
|
||||
|
||||
@@ -25,42 +25,13 @@
|
||||
:title="
|
||||
isRearrange
|
||||
? hasSiteInventoryRecords
|
||||
? '运输已调整,司机与现场链接已更新,旧链接已失效'
|
||||
: '运输已调整,司机链接已更新,旧链接已失效'
|
||||
? '运输已调整,司机与现场链接已更新,旧链接已失效。请在下方「运输信息」中复制司机链接。'
|
||||
: '运输已调整,司机链接已更新,旧链接已失效。请在下方「运输信息」中复制司机链接。'
|
||||
: hasSiteInventoryRecords
|
||||
? '运输已安排,可复制司机链接、现场链接或简述发给相关人员'
|
||||
: '运输已安排,可复制司机链接或计划简述'
|
||||
? '运输已安排,请在下方「运输信息」中复制链接。'
|
||||
: '运输已安排,请在下方「运输信息」中复制司机链接。'
|
||||
"
|
||||
/>
|
||||
<div class="pl-arrange-link-row">
|
||||
<span class="pl-arrange-link-label">司机链接</span>
|
||||
<el-input :model-value="driverUrl || arrangeResult?.driverUrl || ''" readonly />
|
||||
<el-button @click="copyDriverUrl(arrangeResult?.driverUrl)">
|
||||
<el-icon><CopyDocument /></el-icon>复制链接
|
||||
</el-button>
|
||||
</div>
|
||||
<template v-if="hasSiteInventoryRecords">
|
||||
<div v-if="siteRecords.length === 1" class="pl-arrange-link-row">
|
||||
<span class="pl-arrange-link-label">现场链接</span>
|
||||
<el-input :model-value="siteRecords[0].siteUrl || ''" readonly />
|
||||
<el-button @click="copySiteUrl(siteRecords[0].siteUrl)">
|
||||
<el-icon><CopyDocument /></el-icon>复制链接
|
||||
</el-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="record in siteRecords"
|
||||
:key="record.siteLinkId"
|
||||
class="pl-arrange-link-row"
|
||||
>
|
||||
<span class="pl-arrange-link-label">{{ record.productName || '现场链接' }}</span>
|
||||
<el-input :model-value="record.siteUrl || ''" readonly />
|
||||
<el-button @click="copySiteUrl(record.siteUrl)">
|
||||
<el-icon><CopyDocument /></el-icon>复制本条
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section v-else class="pl-plan-form-section">
|
||||
@@ -73,7 +44,7 @@
|
||||
:title="rearrangeBlockedMessage"
|
||||
class="pl-arrange-block-alert"
|
||||
/>
|
||||
<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">
|
||||
<h4 class="pl-plan-subtitle">车辆信息</h4>
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="选择车辆" prop="vehicleId">
|
||||
@@ -130,8 +101,8 @@
|
||||
<div class="pl-plan-logistics-row">
|
||||
<div class="pl-plan-logistics-block">
|
||||
<h4 class="pl-plan-subtitle">取货信息</h4>
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="取货地址">
|
||||
<div class="pl-plan-logistics-fields">
|
||||
<el-form-item label="取货地址" class="pl-logistics-address-item">
|
||||
<PlAddressWithLibrary
|
||||
v-model="formData.pickupAddress"
|
||||
v-model:lng="formData.pickupLng"
|
||||
@@ -139,18 +110,20 @@
|
||||
placeholder="取货地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="取货时间">
|
||||
<el-date-picker
|
||||
v-model="formData.pickupTime"
|
||||
type="date"
|
||||
placeholder="选择取货时间"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="注意事项">
|
||||
<el-input v-model="formData.pickupNotes" placeholder="取货注意事项" />
|
||||
</el-form-item>
|
||||
<div class="form-grid-2 pl-logistics-meta-row">
|
||||
<el-form-item label="取货时间">
|
||||
<el-date-picker
|
||||
v-model="formData.pickupTime"
|
||||
type="date"
|
||||
placeholder="选择取货时间"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="注意事项">
|
||||
<el-input v-model="formData.pickupNotes" placeholder="取货注意事项" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,8 +150,8 @@
|
||||
|
||||
<div class="pl-plan-logistics-block">
|
||||
<h4 class="pl-plan-subtitle">送货信息</h4>
|
||||
<div class="form-grid-3">
|
||||
<el-form-item label="送货地址">
|
||||
<div class="pl-plan-logistics-fields">
|
||||
<el-form-item label="送货地址" class="pl-logistics-address-item">
|
||||
<PlAddressWithLibrary
|
||||
v-model="formData.destinationAddress"
|
||||
v-model:lng="formData.destinationLng"
|
||||
@@ -186,71 +159,27 @@
|
||||
placeholder="送货地址"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="送达时间">
|
||||
<el-date-picker
|
||||
v-model="formData.deliveryTime"
|
||||
type="date"
|
||||
placeholder="选择送达时间"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="注意事项">
|
||||
<el-input v-model="formData.destinationNotes" placeholder="送货注意事项" />
|
||||
</el-form-item>
|
||||
<div class="form-grid-2 pl-logistics-meta-row">
|
||||
<el-form-item label="送达时间">
|
||||
<el-date-picker
|
||||
v-model="formData.deliveryTime"
|
||||
type="date"
|
||||
placeholder="选择送达时间"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="注意事项">
|
||||
<el-input v-model="formData.destinationNotes" placeholder="送货注意事项" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="pl-plan-subtitle">运费信息</h4>
|
||||
<div v-if="formData.hasFreight !== 1" class="pl-arrange-freight-muted">本计划无运输费用</div>
|
||||
<div v-else class="form-grid-4">
|
||||
<el-form-item label="运输公里数">
|
||||
<el-input-number v-model="formData.distanceKm" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="运费计算方式">
|
||||
<el-select v-model="formData.freightType">
|
||||
<el-option label="按重量" value="by_weight" />
|
||||
<el-option label="按趟" value="by_trip" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.freightType === 'by_weight'"
|
||||
:label="`运输量(${freightUnitLabel})`"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="freightCalc.transportQty.value"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
style="width: 100%"
|
||||
@change="freightCalc.markTransportQtyTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.freightType === 'by_weight'"
|
||||
:label="`运输单价(元/${freightUnitLabel})`"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightPrice.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运费金额(元)">
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightAmount.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
@change="freightCalc.markFreightAmountTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<h4 class="pl-plan-subtitle">安排项</h4>
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="运输介质">
|
||||
<el-form-item label="运输介质" class="pl-arrange-medium-item">
|
||||
<el-input :model-value="transportMediumText" readonly placeholder="—" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" class="pl-arrange-remark-item">
|
||||
@@ -287,9 +216,6 @@
|
||||
勾选后将按产品进行地磅单生成(当月已有则复用),并在地磅出入库管理中产生对应记录。
|
||||
</template>
|
||||
</p>
|
||||
<p v-if="planSupportsSiteInventory" class="pl-arrange-dibang-hint">
|
||||
入库/出库计划安排运输时将自动生成工厂现场链接(与是否勾选地磅单无关),不会在地磅出入库管理中产生记录。
|
||||
</p>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
@@ -298,7 +224,15 @@
|
||||
|
||||
<section class="pl-arrange-view-panel">
|
||||
<h3 class="pl-arrange-view-title">计划单查看</h3>
|
||||
<PlPlanViewTabs v-if="planId" ref="tabsRef" :plan-id="planId" embedded />
|
||||
<PlPlanViewTabs
|
||||
v-if="planId"
|
||||
ref="tabsRef"
|
||||
:plan-id="planId"
|
||||
embedded
|
||||
:show-arrange-links="arrangeSuccess"
|
||||
:arrange-driver-url="driverUrl || arrangeResult?.driverUrl || ''"
|
||||
:arrange-site-records="siteRecords"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -345,12 +279,6 @@
|
||||
>
|
||||
<el-icon><CopyDocument /></el-icon>复制给现场
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasSiteInventoryRecords && siteRecords.length > 1"
|
||||
@click="copyAllSiteUrls()"
|
||||
>
|
||||
<el-icon><CopyDocument /></el-icon>复制全部现场链接
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasDibangInventoryRecords"
|
||||
type="primary"
|
||||
@@ -393,9 +321,9 @@ import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-stri
|
||||
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
|
||||
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { usePlFreightCalc, resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
|
||||
import { usePlFreightCalc } from '@/composables/usePlFreightCalc'
|
||||
import { loadVehicleViewBundle, type VehicleViewBundle } from '@/composables/usePlVehicleViewDetail'
|
||||
import { buildMissingHazardCertMessage } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { buildMissingHazardCertReadyMessage } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { usePlPlanArrangeActions } from '@/composables/usePlPlanArrangeActions'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { isTonProductUnit } from '@/utils/crm-product-display'
|
||||
@@ -438,8 +366,6 @@ const {
|
||||
resetArrangeActions,
|
||||
applyArrangeResult,
|
||||
copyDriverUrl,
|
||||
copySiteUrl,
|
||||
copyAllSiteUrls,
|
||||
copySiteSummary,
|
||||
copyPlanSummary,
|
||||
copyPoundSlipQrcode
|
||||
@@ -492,8 +418,6 @@ const dialogTitle = computed(() => {
|
||||
|
||||
const isRearrange = computed(() => plan.value.transportStatus === 'arranged')
|
||||
|
||||
const freightUnitLabel = computed(() => resolveFreightUnitLabel(productRows.value))
|
||||
|
||||
function getUnitLabel(unit: string) {
|
||||
return dictStore.getLabel('crm_product_unit', unit) || unit
|
||||
}
|
||||
@@ -811,9 +735,9 @@ async function handleSubmit() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
const certMessage = buildMissingHazardCertMessage(products.value)
|
||||
const certMessage = buildMissingHazardCertReadyMessage(plan.value, products.value)
|
||||
if (certMessage) {
|
||||
await ElMessageBox.alert(`${certMessage}。请先上传购买证和合法使用证明。`, '无法安排运输', {
|
||||
await ElMessageBox.alert(`${certMessage},请至【危化品证上传】页面操作。`, '无法安排运输', {
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
@@ -884,7 +808,8 @@ async function handleSubmit() {
|
||||
applyArrangeResult(result, plan.value, products.value)
|
||||
arrangeSuccess.value = true
|
||||
ElMessage.success(isRearrange.value ? '运输调整成功' : '运输安排成功')
|
||||
tabsRef.value?.reload()
|
||||
await tabsRef.value?.reload()
|
||||
tabsRef.value?.setActiveTab('transport')
|
||||
emit('success')
|
||||
} catch (e: unknown) {
|
||||
if (e === 'cancel') return
|
||||
@@ -899,20 +824,18 @@ async function handleSubmit() {
|
||||
<style scoped lang="scss">
|
||||
.pl-arrange-layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 480px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pl-arrange-form-panel {
|
||||
flex: 0 0 auto;
|
||||
max-height: 52vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pl-arrange-form-scroll {
|
||||
max-height: 52vh;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
@@ -931,10 +854,18 @@ async function handleSubmit() {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.pl-arrange-freight-muted {
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
.pl-plan-logistics-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pl-logistics-address-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pl-logistics-meta-row {
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.pl-plan-logistics-row {
|
||||
@@ -1004,10 +935,6 @@ async function handleSubmit() {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-grid-3 {
|
||||
gap: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
@@ -1029,13 +956,40 @@ async function handleSubmit() {
|
||||
|
||||
.pl-arrange-view-panel {
|
||||
flex: 1 1 auto;
|
||||
min-height: 280px;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
padding: 12px 16px 16px;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.pl-plan-view-tabs) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.pl-plan-view-tabs__nav) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.pl-plan-view-tabs__nav > .el-tabs__header) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.pl-plan-view-tabs__nav > .el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-arrange-view-title {
|
||||
@@ -1090,6 +1044,7 @@ async function handleSubmit() {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-arrange-medium-item,
|
||||
.pl-arrange-remark-item {
|
||||
grid-column: span 2;
|
||||
}
|
||||
@@ -1100,18 +1055,6 @@ async function handleSubmit() {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pl-arrange-link-row {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pl-arrange-link-label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-arrange-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1125,4 +1068,11 @@ async function handleSubmit() {
|
||||
width: min(1840px, calc(100vw - 48px)) !important;
|
||||
max-height: calc(100vh - 48px);
|
||||
}
|
||||
|
||||
.el-dialog.pl-plan-arrange-dialog.dialog-form-layout .el-dialog__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -19,7 +19,7 @@
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="易制毒或易制爆产品须上传购买证、合法使用证明;安排运输时将校验是否齐全。"
|
||||
title="易制毒或易制爆产品请按实际业务上传所需证书;可先点击「证书齐备」放行运输,之后再补充附件。"
|
||||
class="cert-tip"
|
||||
/>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<el-tag v-if="item.isToxic" type="warning" size="small">易制毒</el-tag>
|
||||
</div>
|
||||
<div class="form-grid-2 product-cert-grid">
|
||||
<el-form-item label="购买证" required>
|
||||
<el-form-item label="购买证">
|
||||
<AttachmentUploadField
|
||||
v-model="item.purchaseCertUrl"
|
||||
folder="pl/plan-cert"
|
||||
@@ -40,7 +40,7 @@
|
||||
hint="支持图片、PDF 等,上传购买证"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合法使用证明" required>
|
||||
<el-form-item label="合法使用证明">
|
||||
<AttachmentUploadField
|
||||
v-model="item.transportCertUrl"
|
||||
folder="pl/plan-cert"
|
||||
@@ -67,7 +67,7 @@
|
||||
import { Check, Close } from '@element-plus/icons-vue'
|
||||
import { planApi, type Plan, type PlanProduct } from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { enrichPlanProductsHazardFromCrm, isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
|
||||
|
||||
type HazardProductRow = PlanProduct & {
|
||||
purchaseCertUrl?: string
|
||||
@@ -114,7 +114,8 @@ async function loadPlan(planId: string) {
|
||||
const data = (res.data ?? res) as Plan
|
||||
planBrief.planNo = data.planNo || ''
|
||||
planBrief.customerName = data.customerName || ''
|
||||
hazardProducts.value = (data.products || [])
|
||||
const enrichedProducts = await enrichPlanProductsHazardFromCrm(data.products || [])
|
||||
hazardProducts.value = enrichedProducts
|
||||
.filter(p => isHazardProduct(p))
|
||||
.map(p => ({
|
||||
...p,
|
||||
@@ -129,14 +130,6 @@ async function loadPlan(planId: string) {
|
||||
async function handleSubmit() {
|
||||
if (!props.planId || !hazardProducts.value.length) return
|
||||
|
||||
const incomplete = hazardProducts.value.filter(
|
||||
p => !p.purchaseCertUrl?.trim() || !p.transportCertUrl?.trim()
|
||||
)
|
||||
if (incomplete.length) {
|
||||
ElMessage.warning('请为每个危化品产品上传购买证和合法使用证明')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await planApi.updateProductCerts(
|
||||
|
||||
@@ -62,6 +62,25 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-else prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
v-if="showSelect"
|
||||
label="操作"
|
||||
min-width="100"
|
||||
align="center"
|
||||
fixed="right"
|
||||
class-name="col-actions"
|
||||
>
|
||||
<template #default="{ row, $index }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
:disabled="isProductSelected(row)"
|
||||
@click="emit('select', row, $index)"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>选择
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="showDelete" label="操作" min-width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button link type="danger" @click="emit('remove', $index)">
|
||||
@@ -75,15 +94,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Delete } from '@element-plus/icons-vue'
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { getDeliveryMethodLabel } from '@/composables/usePlContractToPlan'
|
||||
import PlProductNameHazardCell from '@/components/business/pl-product-name-hazard-cell/index.vue'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
products: PlanFormProductRow[]
|
||||
showDelete?: boolean
|
||||
showSelect?: boolean
|
||||
/** 已加入运输计划的产品 ID,用于禁用合同预览中的「选择」按钮 */
|
||||
selectedProductIds?: string[]
|
||||
editableRemark?: boolean
|
||||
editableQty?: boolean
|
||||
/** 新增/编辑弹窗默认不展示实际数量 */
|
||||
@@ -94,15 +116,20 @@ defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
remove: [index: number]
|
||||
select: [row: PlanFormProductRow, index: number]
|
||||
}>()
|
||||
|
||||
const selectedProductIdSet = computed(
|
||||
() => new Set((props.selectedProductIds || []).map(id => String(id)))
|
||||
)
|
||||
|
||||
function isProductSelected(row: PlanFormProductRow) {
|
||||
return selectedProductIdSet.value.has(String(row.productId))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-plan-product-table-wrap {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dialog-list-body-only {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="pl-plan-form-body">
|
||||
<section class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">基础信息</h3>
|
||||
<el-form ref="basicFormRef" :model="formData" :rules="basicRules" label-position="top">
|
||||
<el-form ref="basicFormRef" :model="formData" :rules="basicRules" label-position="right" label-width="100px" class="pl-dialog-form">
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="计划日期" prop="planDate">
|
||||
<el-date-picker
|
||||
@@ -74,13 +74,19 @@
|
||||
</div>
|
||||
|
||||
<h4 class="pl-plan-subtitle">合同产品预览</h4>
|
||||
<PlPlanFormProductTable :products="contractProductRows" />
|
||||
<p class="pl-plan-form-empty-hint">点击「选择」将合同产品加入下方产品信息;已添加的产品不可重复选择</p>
|
||||
<PlPlanFormProductTable
|
||||
:products="contractProductRows"
|
||||
show-select
|
||||
:selected-product-ids="selectedPlanProductIds"
|
||||
@select="handleSelectContractProduct"
|
||||
/>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<section class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">运输信息</h3>
|
||||
<el-form :model="formData" label-position="top">
|
||||
<el-form :model="formData" label-position="right" label-width="100px" class="pl-dialog-form">
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="车牌号">
|
||||
<el-select
|
||||
@@ -194,43 +200,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="pl-plan-subtitle">运费信息</h4>
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="是否有运输费用">
|
||||
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<template v-if="formData.hasFreight === 1">
|
||||
<el-form-item label="运输公里数">
|
||||
<el-input-number v-model="formData.distanceKm" :min="0" :precision="2" style="width: 100%" />
|
||||
<div class="pl-plan-freight-section">
|
||||
<h4 class="pl-plan-subtitle">运费信息</h4>
|
||||
<div class="form-grid-4">
|
||||
<el-form-item label="是否有运输费用">
|
||||
<el-switch v-model="formData.hasFreight" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="运费计算方式">
|
||||
<el-select v-model="formData.freightType">
|
||||
<el-option label="按重量" value="by_weight" />
|
||||
<el-option label="按趟" value="by_trip" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.freightType === 'by_weight'" :label="`运输量(${freightUnitLabel})`">
|
||||
<el-input-number
|
||||
v-model="freightCalc.transportQty.value"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
style="width: 100%"
|
||||
@change="freightCalc.markTransportQtyTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.freightType === 'by_weight'" :label="`运输单价(元/${freightUnitLabel})`">
|
||||
<el-input-number v-model="freightCalc.freightPrice.value" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="运费金额(元)">
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightAmount.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
@change="freightCalc.markFreightAmountTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<template v-if="formData.hasFreight === 1">
|
||||
<el-form-item label="运输公里数">
|
||||
<el-input-number v-model="formData.distanceKm" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="运费计算方式">
|
||||
<el-select v-model="formData.freightType">
|
||||
<el-option label="按重量" value="by_weight" />
|
||||
<el-option label="按趟" value="by_trip" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.freightType === 'by_weight'" :label="`运输量(${freightUnitLabel})`">
|
||||
<el-input-number
|
||||
v-model="freightCalc.transportQty.value"
|
||||
:min="0"
|
||||
:precision="3"
|
||||
style="width: 100%"
|
||||
@change="freightCalc.markTransportQtyTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.freightType === 'by_weight'" :label="`运输单价(元/${freightUnitLabel})`">
|
||||
<el-input-number v-model="freightCalc.freightPrice.value" :min="0" :precision="2" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="运费金额(元)">
|
||||
<el-input-number
|
||||
v-model="freightCalc.freightAmount.value"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
@change="freightCalc.markFreightAmountTouched()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</section>
|
||||
@@ -467,6 +475,10 @@ const contractProductRows = computed<PlanFormProductRow[]>(() =>
|
||||
mapContractProducts(contractSnapshot.value?.products)
|
||||
)
|
||||
|
||||
const selectedPlanProductIds = computed(() =>
|
||||
formData.products.map(item => String(item.productId))
|
||||
)
|
||||
|
||||
function enrichProductsFromContract(products: PlanFormProductRow[], contract?: ContractVO | null) {
|
||||
if (!contract?.products?.length) return products
|
||||
const contractMap = new Map(
|
||||
@@ -845,12 +857,24 @@ async function handleContractConfirm(contract: ContractVO) {
|
||||
contractSnapshot.value = contract
|
||||
Object.assign(formData, {
|
||||
...mapped,
|
||||
products: [],
|
||||
followUserId: resolveCurrentUserId(),
|
||||
followUserName: resolveCurrentUserName()
|
||||
})
|
||||
formData.products = await enrichPlanProductsHazardFromCrm(
|
||||
enrichProductsFromContract(formData.products, contract)
|
||||
freightCalc.resetFreightCalc()
|
||||
}
|
||||
|
||||
async function handleSelectContractProduct(row: PlanFormProductRow) {
|
||||
const productId = String(row.productId)
|
||||
if (formData.products.some(item => String(item.productId) === productId)) {
|
||||
ElMessage.warning('该产品已添加到运输计划中')
|
||||
return
|
||||
}
|
||||
|
||||
const [enriched] = await enrichPlanProductsHazardFromCrm(
|
||||
enrichProductsFromContract([{ ...row }], contractSnapshot.value)
|
||||
)
|
||||
formData.products.push(enriched)
|
||||
freightCalc.resetFreightCalc()
|
||||
}
|
||||
|
||||
@@ -994,11 +1018,22 @@ async function handleSubmit() {
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.pl-plan-freight-section {
|
||||
.pl-plan-subtitle {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
width: 140px !important;
|
||||
max-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-form-empty-hint,
|
||||
.text-muted {
|
||||
margin: 0;
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-plan-contract-kv-rows {
|
||||
@@ -1166,7 +1201,7 @@ async function handleSubmit() {
|
||||
<style lang="scss">
|
||||
.el-dialog.pl-plan-form-dialog.dialog-form-layout {
|
||||
width: min(1680px, calc(100vw - 80px)) !important;
|
||||
max-height: calc(100vh - 48px);
|
||||
max-height: var(--dialog-max-height, calc(100vh - 40px));
|
||||
}
|
||||
|
||||
.el-dialog.pl-plan-form-dialog.dialog-form-layout .el-dialog__body {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div class="pl-plan-hazard-cert-view-table">
|
||||
<p v-if="!products.length" class="pl-plan-hazard-cert-view-table__empty">暂无易制毒/易制爆产品</p>
|
||||
|
||||
<div v-else class="panel dialog-list-panel">
|
||||
<div class="list-body list-wrap dialog-list-body-only">
|
||||
<el-table
|
||||
:data="products"
|
||||
class="list-table"
|
||||
stripe
|
||||
empty-text="暂无易制毒/易制爆产品"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="60"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="index => index + 1"
|
||||
/>
|
||||
<el-table-column label="产品名称" min-width="160" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<PlProductNameHazardCell :product="row" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="购买证" min-width="160" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.purchaseCertUrl" class="pl-plan-hazard-cert-view-table__attachment">
|
||||
<AttachmentUploadField :model-value="row.purchaseCertUrl" disabled compact />
|
||||
</div>
|
||||
<span v-else class="pl-plan-hazard-cert-view-table__missing">未上传</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="合法使用证明" min-width="160" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.transportCertUrl" class="pl-plan-hazard-cert-view-table__attachment">
|
||||
<AttachmentUploadField :model-value="row.transportCertUrl" disabled compact />
|
||||
</div>
|
||||
<span v-else class="pl-plan-hazard-cert-view-table__missing">未上传</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PlanProduct } from '@/api/pl'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlProductNameHazardCell from '@/components/business/pl-product-name-hazard-cell/index.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
products?: PlanProduct[]
|
||||
}>(),
|
||||
{
|
||||
products: () => []
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.pl-plan-hazard-cert-view-table__empty {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
.pl-plan-hazard-cert-view-table__attachment {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
:deep(.attachment-upload) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
:deep(.attachment-list) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-hazard-cert-view-table__missing {
|
||||
color: var(--mint-text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div v-if="hazardProducts.length" class="pl-plan-list-cert-cell">
|
||||
<div
|
||||
v-if="hazardProducts.length"
|
||||
class="pl-plan-list-cert-cell"
|
||||
:class="{ 'pl-plan-list-cert-cell--compact': compact }"
|
||||
>
|
||||
<div
|
||||
v-for="(product, index) in hazardProducts"
|
||||
:key="product.id || `${product.productNo}-${index}`"
|
||||
@@ -7,10 +11,22 @@
|
||||
>
|
||||
<div class="pl-plan-list-cert-cell__product">
|
||||
<span class="pl-plan-list-cert-cell__name">{{ product.productName || '—' }}</span>
|
||||
<el-tag v-if="product.isToxic" type="warning" size="small">易制毒</el-tag>
|
||||
<el-tag v-if="product.isExplosive" type="danger" size="small">易制爆</el-tag>
|
||||
<el-tag v-if="product.isToxic" type="warning" size="small">
|
||||
{{ useShortHazardTag ? '毒' : '易制毒' }}
|
||||
</el-tag>
|
||||
<el-tag v-if="product.isExplosive" type="danger" size="small">
|
||||
{{ useShortHazardTag ? '爆' : '易制爆' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="pl-plan-list-cert-cell__certs">
|
||||
<div v-if="compact" class="pl-plan-list-cert-cell__certs pl-plan-list-cert-cell__certs--text">
|
||||
<span class="pl-plan-list-cert-cell__cert-text">
|
||||
购买证({{ product.purchaseCertUrl ? '有' : '无' }})
|
||||
</span>
|
||||
<span class="pl-plan-list-cert-cell__cert-text">
|
||||
合法使用证明({{ product.transportCertUrl ? '有' : '无' }})
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="pl-plan-list-cert-cell__certs">
|
||||
<div class="pl-plan-list-cert-cell__cert-item">
|
||||
<AttachmentUploadField
|
||||
v-if="product.purchaseCertUrl"
|
||||
@@ -44,12 +60,20 @@ import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
products?: PlanProduct[]
|
||||
/** 列表紧凑模式:文字展示证书有无,不显示缩略图 */
|
||||
compact?: boolean
|
||||
/** 危化标签短文案:毒 / 爆 */
|
||||
shortHazardTag?: boolean
|
||||
}>(),
|
||||
{
|
||||
products: () => []
|
||||
products: () => [],
|
||||
compact: false,
|
||||
shortHazardTag: false
|
||||
}
|
||||
)
|
||||
|
||||
const useShortHazardTag = computed(() => props.compact || props.shortHazardTag)
|
||||
|
||||
const hazardProducts = computed(() => (props.products || []).filter(product => isHazardProduct(product)))
|
||||
</script>
|
||||
|
||||
@@ -137,4 +161,29 @@ const hazardProducts = computed(() => (props.products || []).filter(product => i
|
||||
:deep(.attachment-footer) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pl-plan-list-cert-cell--compact {
|
||||
gap: 6px;
|
||||
|
||||
.pl-plan-list-cert-cell__group {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.pl-plan-list-cert-cell__name {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.pl-plan-list-cert-cell__certs--text {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.pl-plan-list-cert-cell__cert-text {
|
||||
color: var(--mint-text);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,20 +5,40 @@
|
||||
:key="product.id || `${product.productNo}-${index}`"
|
||||
class="pl-plan-list-product-cell__row"
|
||||
>
|
||||
<span class="pl-plan-list-product-cell__line">
|
||||
<span class="pl-plan-list-product-cell__text">{{ formatProductLine(product) }}</span>
|
||||
<el-tag v-if="getHazardFlags(product).isToxic" type="warning" size="small" class="pl-plan-list-product-cell__mark">
|
||||
毒
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="getHazardFlags(product).isExplosive"
|
||||
type="danger"
|
||||
size="small"
|
||||
class="pl-plan-list-product-cell__mark"
|
||||
<span class="pl-plan-list-product-cell__index">{{ index + 1 }}.</span>
|
||||
<div class="pl-plan-list-product-cell__content">
|
||||
<span class="pl-plan-list-product-cell__line">
|
||||
<span class="pl-plan-list-product-cell__text">{{ formatProductLine(product) }}</span>
|
||||
<el-tag v-if="getHazardFlags(product).isToxic" type="warning" size="small" class="pl-plan-list-product-cell__mark">
|
||||
毒
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="getHazardFlags(product).isExplosive"
|
||||
type="danger"
|
||||
size="small"
|
||||
class="pl-plan-list-product-cell__mark"
|
||||
>
|
||||
爆
|
||||
</el-tag>
|
||||
</span>
|
||||
<div
|
||||
v-if="showCertStatus && isHazardProduct(product)"
|
||||
class="pl-plan-list-product-cell__certs"
|
||||
>
|
||||
爆
|
||||
</el-tag>
|
||||
</span>
|
||||
<span
|
||||
class="pl-plan-list-product-cell__cert-text"
|
||||
:class="{ 'pl-plan-list-product-cell__cert-text--missing': !product.purchaseCertUrl }"
|
||||
>
|
||||
购买证({{ product.purchaseCertUrl ? '有' : '无' }})
|
||||
</span>
|
||||
<span
|
||||
class="pl-plan-list-product-cell__cert-text"
|
||||
:class="{ 'pl-plan-list-product-cell__cert-text--missing': !product.transportCertUrl }"
|
||||
>
|
||||
合法使用证明({{ product.transportCertUrl ? '有' : '无' }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="pl-plan-list-product-cell__empty">—</span>
|
||||
@@ -26,16 +46,19 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PlanProduct } from '@/api/pl'
|
||||
import { resolveProductHazardFlags } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { isHazardProduct, resolveProductHazardFlags } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { formatPlanProductListText } from '@/utils/pl-display'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
products?: PlanProduct[]
|
||||
/** 危化品行下方展示购买证/合法使用证明有无 */
|
||||
showCertStatus?: boolean
|
||||
}>(),
|
||||
{
|
||||
products: () => []
|
||||
products: () => [],
|
||||
showCertStatus: false
|
||||
}
|
||||
)
|
||||
|
||||
@@ -58,22 +81,47 @@ function getHazardFlags(product: PlanProduct) {
|
||||
.pl-plan-list-product-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pl-plan-list-product-cell__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-plan-list-product-cell__index {
|
||||
flex-shrink: 0;
|
||||
min-width: 1.25em;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--mint-text);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.pl-plan-list-product-cell__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.pl-plan-list-product-cell__line {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--text, #1e293b);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pl-plan-list-product-cell__text {
|
||||
@@ -85,6 +133,30 @@ function getHazardFlags(product: PlanProduct) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pl-plan-list-product-cell__certs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pl-plan-list-product-cell__cert-text {
|
||||
color: var(--mint-text);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
|
||||
&--missing {
|
||||
color: var(--danger, #dc2626);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-list-product-cell__empty {
|
||||
color: var(--mint-text);
|
||||
}
|
||||
|
||||
@@ -13,50 +13,32 @@
|
||||
@closed="handleClosed"
|
||||
@opened="handleOpened"
|
||||
>
|
||||
<section v-if="showArrangeActions" class="pl-plan-arrange-summary">
|
||||
<h3 class="pl-plan-arrange-summary-title">运输安排结果</h3>
|
||||
<div class="pl-arrange-link-row">
|
||||
<span class="pl-arrange-link-label">司机链接</span>
|
||||
<el-input :model-value="driverUrl" readonly />
|
||||
<el-button @click="copyDriverUrl()">
|
||||
<el-icon><CopyDocument /></el-icon>复制链接
|
||||
<div
|
||||
v-if="$slots['hero-actions'] || showArrangeActions"
|
||||
class="pl-plan-view-hero-actions"
|
||||
>
|
||||
<template v-if="showArrangeActions">
|
||||
<el-button @click="copyPlanSummary()">
|
||||
<el-icon><CopyDocument /></el-icon>复制给司机
|
||||
</el-button>
|
||||
<el-button v-if="hasSiteInventoryRecords" @click="copySiteSummary()">
|
||||
<el-icon><CopyDocument /></el-icon>复制给现场
|
||||
</el-button>
|
||||
<el-button v-if="hasDibangInventoryRecords" @click="copyPoundSlipQrcode()">
|
||||
<el-icon><CopyDocument /></el-icon>复制磅单二维码
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<p v-if="!hasSiteInventoryRecords && planSupportsInventory" class="pl-plan-arrange-summary-hint">
|
||||
暂无现场链接
|
||||
</p>
|
||||
|
||||
<template v-if="hasSiteInventoryRecords">
|
||||
<h4 class="pl-plan-arrange-summary-subtitle">现场链接</h4>
|
||||
<div v-if="siteRecords.length === 1" class="pl-arrange-link-row">
|
||||
<span class="pl-arrange-link-label">现场链接</span>
|
||||
<el-input :model-value="siteRecords[0].siteUrl || ''" readonly />
|
||||
<el-button @click="copySiteUrl(siteRecords[0].siteUrl)">
|
||||
<el-icon><CopyDocument /></el-icon>复制链接
|
||||
</el-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="record in siteRecords"
|
||||
:key="record.siteLinkId"
|
||||
class="pl-arrange-link-row"
|
||||
>
|
||||
<span class="pl-arrange-link-label">{{ record.productName || '现场链接' }}</span>
|
||||
<el-input :model-value="record.siteUrl || ''" readonly />
|
||||
<el-button @click="copySiteUrl(record.siteUrl)">
|
||||
<el-icon><CopyDocument /></el-icon>复制本条
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<div v-if="$slots['hero-actions']" class="pl-plan-view-hero-actions">
|
||||
<slot name="hero-actions" />
|
||||
</div>
|
||||
|
||||
<PlPlanViewTabs v-if="planId" ref="tabsRef" :plan-id="planId" />
|
||||
<PlPlanViewTabs
|
||||
v-if="planId"
|
||||
ref="tabsRef"
|
||||
:plan-id="planId"
|
||||
:show-arrange-links="showArrangeActions"
|
||||
:arrange-driver-url="driverUrl"
|
||||
:arrange-site-records="siteRecords"
|
||||
/>
|
||||
|
||||
<DbPoundSlipQrcodeDialog
|
||||
v-model:visible="qrcodeVisible"
|
||||
@@ -65,39 +47,11 @@
|
||||
ref="qrcodeDialogRef"
|
||||
/>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer pl-plan-view-footer">
|
||||
<template v-if="showArrangeActions">
|
||||
<el-button @click="copyPlanSummary()">
|
||||
<el-icon><CopyDocument /></el-icon>复制给司机
|
||||
</el-button>
|
||||
<el-button v-if="hasSiteInventoryRecords" @click="copySiteSummary()">
|
||||
<el-icon><CopyDocument /></el-icon>复制给现场
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasSiteInventoryRecords && siteRecords.length > 1"
|
||||
@click="copyAllSiteUrls()"
|
||||
>
|
||||
<el-icon><CopyDocument /></el-icon>复制全部现场链接
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasDibangInventoryRecords"
|
||||
type="primary"
|
||||
@click="copyPoundSlipQrcode()"
|
||||
>
|
||||
<el-icon><CopyDocument /></el-icon>复制磅单二维码
|
||||
</el-button>
|
||||
</template>
|
||||
<el-button @click="innerVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close, CopyDocument } from '@element-plus/icons-vue'
|
||||
import { CopyDocument } from '@element-plus/icons-vue'
|
||||
import { planApi } from '@/api/pl'
|
||||
import DbPoundSlipQrcodeDialog from '@/components/business/db/DbPoundSlipQrcodeDialog.vue'
|
||||
import PlPlanViewTabs from '@/components/business/pl-plan-view-tabs/index.vue'
|
||||
@@ -126,27 +80,17 @@ const {
|
||||
hasDibangInventoryRecords,
|
||||
driverUrl,
|
||||
siteRecords,
|
||||
inventoryRecords,
|
||||
plan,
|
||||
qrcodeVisible,
|
||||
selectedInventoryId,
|
||||
selectedInventoryUnit,
|
||||
qrcodeDialogRef,
|
||||
resetArrangeActions,
|
||||
loadArrangeActions,
|
||||
copyDriverUrl,
|
||||
copySiteUrl,
|
||||
copyAllSiteUrls,
|
||||
copySiteSummary,
|
||||
copyPlanSummary,
|
||||
copyPoundSlipQrcode
|
||||
} = usePlPlanArrangeActions()
|
||||
|
||||
const planSupportsInventory = computed(() => {
|
||||
const planType = plan.value?.planType
|
||||
return planType === 'inbound' || planType === 'outbound'
|
||||
})
|
||||
|
||||
const dialogTitle = computed(() =>
|
||||
planNo.value ? `查看运输计划 · ${planNo.value}` : '查看运输计划'
|
||||
)
|
||||
@@ -168,6 +112,21 @@ function handleClosed() {
|
||||
planNo.value = ''
|
||||
resetArrangeActions()
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
if (!props.visible || !props.planId) return
|
||||
try {
|
||||
const res = await planApi.get(props.planId)
|
||||
const data = res.data ?? res
|
||||
planNo.value = data?.planNo || ''
|
||||
await loadArrangeActions(props.planId)
|
||||
await tabsRef.value?.reload()
|
||||
} catch {
|
||||
/* 查看弹窗仍打开时静默失败,避免打断用户 */
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ reload })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -186,52 +145,42 @@ function handleClosed() {
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
.pl-plan-arrange-summary {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
<style lang="scss">
|
||||
.el-dialog.pl-plan-view-dialog.dialog-form-layout {
|
||||
width: min(1680px, calc(100vw - 80px)) !important;
|
||||
height: var(--dialog-max-height, calc(100vh - 40px));
|
||||
max-height: var(--dialog-max-height, calc(100vh - 40px));
|
||||
}
|
||||
|
||||
.pl-plan-arrange-summary-title {
|
||||
margin: 0 0 10px;
|
||||
padding-left: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
border-left: 3px solid #1ca851;
|
||||
line-height: 1;
|
||||
.el-dialog.pl-plan-view-dialog.dialog-form-layout .el-dialog__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.pl-plan-arrange-summary-subtitle {
|
||||
margin: 12px 0 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
.el-dialog.pl-plan-view-dialog.dialog-form-layout .pl-plan-view-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pl-arrange-link-row {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
& + & {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.el-dialog.pl-plan-view-dialog.dialog-form-layout .pl-plan-view-tabs__nav.el-tabs {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pl-arrange-link-label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-plan-arrange-summary-hint {
|
||||
margin: 10px 0 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
.el-dialog.pl-plan-view-dialog.dialog-form-layout
|
||||
.pl-plan-view-tabs__nav.el-tabs
|
||||
> .el-tabs__content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@
|
||||
<el-tab-pane label="计划概况" name="overview">
|
||||
<section class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">基础信息</h3>
|
||||
<ViewKvTable :fields="overviewBasicViewFields" mode="quad" />
|
||||
<ViewKvTable :fields="overviewBasicViewFields" mode="quint" />
|
||||
</section>
|
||||
|
||||
<section class="pl-plan-form-section">
|
||||
@@ -16,40 +16,7 @@
|
||||
|
||||
<section class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">危化品证书信息</h3>
|
||||
<p v-if="!hazardProducts.length && !loading" class="pl-plan-form-empty-hint"
|
||||
>暂无易制毒/易制爆产品</p
|
||||
>
|
||||
<div v-for="item in hazardProducts" :key="item.id" class="pl-plan-cert-block">
|
||||
<div class="pl-plan-cert-head">
|
||||
<span class="pl-plan-cert-name">{{ item.productName }}</span>
|
||||
<el-tag v-if="item.isToxic" type="warning" size="small">易制毒</el-tag>
|
||||
<el-tag v-if="item.isExplosive" type="danger" size="small">易制爆</el-tag>
|
||||
</div>
|
||||
<dl class="pl-plan-kv-grid pl-plan-kv-grid-2 pl-plan-cert-grid">
|
||||
<div class="pl-plan-kv">
|
||||
<dt>购买证</dt>
|
||||
<dd>
|
||||
<AttachmentUploadField
|
||||
v-if="item.purchaseCertUrl"
|
||||
:model-value="item.purchaseCertUrl"
|
||||
disabled
|
||||
/>
|
||||
<span v-else>未上传</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>合法使用证明</dt>
|
||||
<dd>
|
||||
<AttachmentUploadField
|
||||
v-if="item.transportCertUrl"
|
||||
:model-value="item.transportCertUrl"
|
||||
disabled
|
||||
/>
|
||||
<span v-else>未上传</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<PlPlanHazardCertViewTable :products="hazardProducts" />
|
||||
</section>
|
||||
|
||||
<section class="pl-plan-form-section">
|
||||
@@ -59,6 +26,45 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="运输信息" name="transport">
|
||||
<section v-if="showArrangeLinks" class="pl-plan-form-section pl-plan-arrange-links">
|
||||
<h3 class="pl-plan-form-section-title">运输链接</h3>
|
||||
<div class="pl-arrange-link-row">
|
||||
<span class="pl-arrange-link-label">司机链接</span>
|
||||
<el-input :model-value="arrangeDriverUrl" readonly />
|
||||
<el-button @click="copyArrangeDriverUrl">
|
||||
<el-icon><CopyDocument /></el-icon>复制链接
|
||||
</el-button>
|
||||
</div>
|
||||
<template v-if="hasArrangeSiteLinks">
|
||||
<div v-if="arrangeSiteRecords.length === 1" class="pl-arrange-link-row">
|
||||
<span class="pl-arrange-link-label">现场链接</span>
|
||||
<el-input :model-value="arrangeSiteRecords[0].siteUrl || ''" readonly />
|
||||
<el-button @click="copyArrangeSiteUrl(arrangeSiteRecords[0].siteUrl)">
|
||||
<el-icon><CopyDocument /></el-icon>复制链接
|
||||
</el-button>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="record in arrangeSiteRecords"
|
||||
:key="record.siteLinkId"
|
||||
class="pl-arrange-link-row"
|
||||
>
|
||||
<span class="pl-arrange-link-label">{{ record.productName || '现场链接' }}</span>
|
||||
<el-input :model-value="record.siteUrl || ''" readonly />
|
||||
<el-button @click="copyArrangeSiteUrl(record.siteUrl)">
|
||||
<el-icon><CopyDocument /></el-icon>复制链接
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<p
|
||||
v-else-if="showArrangeLinks && planSupportsInventory"
|
||||
class="pl-plan-form-empty-hint pl-plan-arrange-links-hint"
|
||||
>
|
||||
暂无现场链接
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="pl-plan-form-section">
|
||||
<h3 class="pl-plan-form-section-title">车辆与承运</h3>
|
||||
<PlVehicleSummaryStrip
|
||||
@@ -110,13 +116,47 @@
|
||||
<div class="pl-plan-feedback-region">
|
||||
<h4 class="pl-plan-feedback-region__title">司机回传信息</h4>
|
||||
<p v-if="!hasDriverFeedback" class="pl-plan-form-empty-hint">暂无司机回传</p>
|
||||
<template v-else>
|
||||
<ViewKvTable :fields="driverFeedbackViewFields" mode="quad" />
|
||||
<div v-if="feedbackImageCsv" class="pl-plan-feedback-attachments">
|
||||
<span class="pl-plan-feedback-attachments__label">回传附件</span>
|
||||
<AttachmentUploadField :model-value="feedbackImageCsv" disabled />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="view-subtable-wrap">
|
||||
<el-table :data="driverFeedbackTableRows" class="view-subtable list-table" stripe>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="feedbackTime"
|
||||
label="回传时间"
|
||||
min-width="160"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="feedbackQty"
|
||||
label="回传数量"
|
||||
min-width="100"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.feedbackQty ?? '—' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="feedbackRemark"
|
||||
label="回传备注"
|
||||
min-width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="回传附件" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.feedbackImages" class="pl-plan-feedback-table__attachment">
|
||||
<AttachmentUploadField :model-value="row.feedbackImages" disabled compact />
|
||||
</div>
|
||||
<span v-else class="pl-plan-feedback-table__empty-cell">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pl-plan-feedback-region">
|
||||
@@ -124,38 +164,69 @@
|
||||
<p v-if="!siteInventoryRows.length && !loading" class="pl-plan-form-empty-hint">
|
||||
暂无现场链接
|
||||
</p>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(row, idx) in siteInventoryRows"
|
||||
:key="`site-feedback-${row.link.siteLinkId || idx}`"
|
||||
class="pl-plan-site-feedback-block"
|
||||
>
|
||||
<div class="pl-plan-site-feedback-head">
|
||||
<span class="pl-plan-dibang-product">{{ row.link.productName || '—' }}</span>
|
||||
<div class="pl-plan-dibang-head-actions">
|
||||
<el-button class="btn-op-item" @click="copySiteSummaryForRow(row)">
|
||||
<div v-else class="view-subtable-wrap">
|
||||
<el-table :data="siteFeedbackTableRows" class="view-subtable list-table" stripe>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="productName"
|
||||
label="产品"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
prop="siteFeedbackTime"
|
||||
label="保存时间"
|
||||
min-width="160"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="业务类型" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
v-if="row.businessType && row.businessType !== '—'"
|
||||
:type="businessTypeTagType(row.businessType)"
|
||||
size="small"
|
||||
>
|
||||
{{ row.businessType }}
|
||||
</el-tag>
|
||||
<span v-else class="pl-plan-feedback-table__empty-cell">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="siteFeedbackRemark"
|
||||
label="现场备注"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="现场附件" min-width="140" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.siteFeedbackImages" class="pl-plan-feedback-table__attachment">
|
||||
<AttachmentUploadField :model-value="row.siteFeedbackImages" disabled compact />
|
||||
</div>
|
||||
<span v-else class="pl-plan-feedback-table__empty-cell">—</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="120"
|
||||
align="center"
|
||||
fixed="right"
|
||||
class-name="col-actions"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="copySiteSummaryForRow(row.sourceRow)">
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
复制给现场
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="copySiteLinkForRow(row)">
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
复制现场链接
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="hasSiteFeedback(row)">
|
||||
<ViewKvTable :fields="siteFeedbackViewFields(row)" mode="quad" />
|
||||
<div
|
||||
v-if="siteFeedbackImageCsv(row)"
|
||||
class="pl-plan-feedback-attachments pl-plan-site-feedback-attachments"
|
||||
>
|
||||
<span class="pl-plan-feedback-attachments__label">现场附件</span>
|
||||
<AttachmentUploadField :model-value="siteFeedbackImageCsv(row)" disabled />
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="pl-plan-form-empty-hint pl-plan-site-feedback-empty">暂无现场回传</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="pl-plan-subtitle">地磅单信息</h4>
|
||||
@@ -355,6 +426,7 @@
|
||||
import { CopyDocument, View } from '@element-plus/icons-vue'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import PlPlanFormProductTable from '@/components/business/pl-plan-form-dialog/PlPlanFormProductTable.vue'
|
||||
import PlPlanHazardCertViewTable from '@/components/business/pl-plan-hazard-cert-view-table/index.vue'
|
||||
import PlVehicleSummaryStrip from '@/components/business/pl-vehicle-summary-strip/index.vue'
|
||||
import PlVehicleViewDialog from '@/components/business/pl-vehicle-view-dialog/index.vue'
|
||||
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
|
||||
@@ -365,8 +437,10 @@
|
||||
} from '@/components/business/db/db-related-view'
|
||||
import type { ContractVO } from '@/api/crm/contract/types'
|
||||
import { inventoryApi } from '@/api/db/inventory'
|
||||
import type { TransportSiteLink } from '@/api/pl'
|
||||
import { mapContractProducts, type PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { mapPlanProductToFormRow, usePlPlanViewDetail, type PlPlanTransportDibangRow, type PlPlanTransportSiteRow } from '@/composables/usePlPlanViewDetail'
|
||||
import { resolveFreightUnitLabel } from '@/composables/usePlFreightCalc'
|
||||
import { isHazardProduct } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { formatDate, formatDateTime } from '@/utils/format'
|
||||
@@ -396,8 +470,16 @@
|
||||
defineProps<{
|
||||
planId: string
|
||||
embedded?: boolean
|
||||
showArrangeLinks?: boolean
|
||||
arrangeDriverUrl?: string
|
||||
arrangeSiteRecords?: TransportSiteLink[]
|
||||
}>(),
|
||||
{ embedded: false }
|
||||
{
|
||||
embedded: false,
|
||||
showArrangeLinks: false,
|
||||
arrangeDriverUrl: '',
|
||||
arrangeSiteRecords: () => []
|
||||
}
|
||||
)
|
||||
|
||||
const activeTab = ref('overview')
|
||||
@@ -436,22 +518,28 @@
|
||||
return Array.isArray(imgs) ? imgs.filter(Boolean).join(',') : ''
|
||||
}
|
||||
|
||||
function hasSiteFeedback(row: PlPlanTransportSiteRow) {
|
||||
return Boolean(
|
||||
row.link.siteFeedbackRemark ||
|
||||
siteFeedbackImageCsv(row) ||
|
||||
row.link.siteFeedbackTime
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function siteFeedbackViewFields(row: PlPlanTransportSiteRow): ViewField[] {
|
||||
const driverFeedbackTableRows = computed(() => {
|
||||
if (!hasDriverFeedback.value) return []
|
||||
return [
|
||||
{ label: '保存时间', value: formatDateTime(row.link.siteFeedbackTime) || '—' },
|
||||
{ label: '业务类型', value: row.link.businessType || '—' },
|
||||
{ label: '现场备注', value: row.link.siteFeedbackRemark, span: 'full' }
|
||||
{
|
||||
feedbackTime: displayFeedbackTime.value,
|
||||
feedbackQty: transport.value?.feedbackQty,
|
||||
feedbackRemark: transport.value?.feedbackRemark || '—',
|
||||
feedbackImages: feedbackImageCsv.value
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const siteFeedbackTableRows = computed(() =>
|
||||
siteInventoryRows.value.map((row) => ({
|
||||
sourceRow: row,
|
||||
productName: row.link.productName || '—',
|
||||
siteFeedbackTime: formatDateTime(row.link.siteFeedbackTime) || '—',
|
||||
businessType: row.link.businessType || '—',
|
||||
siteFeedbackRemark: row.link.siteFeedbackRemark || '—',
|
||||
siteFeedbackImages: siteFeedbackImageCsv(row)
|
||||
}))
|
||||
)
|
||||
|
||||
function dibangBasicViewFields(row: PlPlanTransportDibangRow): ViewField[] {
|
||||
const businessType = row.inventory?.businessType
|
||||
@@ -478,13 +566,32 @@
|
||||
|
||||
const siteInventoryRows = computed(() => siteRows.value)
|
||||
|
||||
async function copySiteLinkForRow(row: PlPlanTransportSiteRow) {
|
||||
const url = row.link.siteUrl?.trim()
|
||||
const hasArrangeSiteLinks = computed(
|
||||
() => props.showArrangeLinks && (props.arrangeSiteRecords?.length ?? 0) > 0
|
||||
)
|
||||
|
||||
const planSupportsInventory = computed(() => {
|
||||
const planType = plan.value?.planType
|
||||
return planType === 'inbound' || planType === 'outbound'
|
||||
})
|
||||
|
||||
async function copyArrangeDriverUrl() {
|
||||
const url = props.arrangeDriverUrl?.trim()
|
||||
if (!url) {
|
||||
ElMessage.warning('暂无现场链接')
|
||||
ElMessage.warning('暂无司机链接')
|
||||
return
|
||||
}
|
||||
await navigator.clipboard.writeText(url)
|
||||
ElMessage.success('已复制司机链接')
|
||||
}
|
||||
|
||||
async function copyArrangeSiteUrl(url?: string) {
|
||||
const target = url?.trim()
|
||||
if (!target) {
|
||||
ElMessage.warning('暂无现场链接')
|
||||
return
|
||||
}
|
||||
await navigator.clipboard.writeText(target)
|
||||
ElMessage.success('已复制现场链接')
|
||||
}
|
||||
|
||||
@@ -548,6 +655,18 @@
|
||||
return amount ? formatMoneyDisplay(Number(amount)) : '—'
|
||||
})
|
||||
|
||||
const freightUnitLabel = computed(() => {
|
||||
const unitCode = resolveFreightUnitLabel(productRows.value)
|
||||
return dictStore.getLabel('crm_product_unit', unitCode) || unitCode || '吨'
|
||||
})
|
||||
|
||||
const displayHasInvoiceLabel = computed(() => {
|
||||
if (transport.value?.hasInvoice != null) {
|
||||
return getPlYesNoLabel(transport.value.hasInvoice)
|
||||
}
|
||||
return getPlHasInvoiceLabelFromSettlement(freightSettlement.value)
|
||||
})
|
||||
|
||||
function mergeTaxRatesFromContract(
|
||||
rows: PlanFormProductRow[],
|
||||
contract: ContractVO | null
|
||||
@@ -576,8 +695,8 @@
|
||||
|
||||
const pickupViewFields = computed<ViewField[]>(() => [
|
||||
{ label: '取货地址', value: plan.value?.pickupAddress, span: 'full' },
|
||||
{ label: '取货时间', value: formatDate(plan.value?.pickupTime) || '—' },
|
||||
{ label: '注意事项', value: plan.value?.pickupNotes, span: 'full' }
|
||||
{ label: '取货时间', value: formatDate(plan.value?.pickupTime) || '—', long: false },
|
||||
{ label: '注意事项', value: plan.value?.pickupNotes, long: false, fullSpan: false }
|
||||
])
|
||||
|
||||
const deliveryViewFields = computed<ViewField[]>(() => [
|
||||
@@ -586,40 +705,38 @@
|
||||
value: plan.value?.destinationAddress || plan.value?.destinationName,
|
||||
span: 'full'
|
||||
},
|
||||
{ label: '送达时间', value: formatDate(plan.value?.deliveryTime) || '—' },
|
||||
{ label: '注意事项', value: plan.value?.destinationNotes, span: 'full' }
|
||||
{ label: '送达时间', value: formatDate(plan.value?.deliveryTime) || '—', long: false },
|
||||
{ label: '注意事项', value: plan.value?.destinationNotes, long: false, fullSpan: false }
|
||||
])
|
||||
|
||||
const freightViewFields = computed<ViewField[]>(() => {
|
||||
const fields: ViewField[] = [
|
||||
{ label: '是否有运输费用', value: getPlYesNoLabel(plan.value?.hasFreight) }
|
||||
const hasFreight = plan.value?.hasFreight === 1
|
||||
const unit = freightUnitLabel.value
|
||||
const isByWeight = hasFreight && displayFreightType.value === 'by_weight'
|
||||
|
||||
return [
|
||||
{ label: '是否有运输费用', value: getPlYesNoLabel(plan.value?.hasFreight) },
|
||||
{ label: '运输公里数', value: hasFreight ? displayDistanceKm.value : '—' },
|
||||
{
|
||||
label: '运费计算方式',
|
||||
value: hasFreight ? getPlFreightTypeLabel(displayFreightType.value) : '—'
|
||||
},
|
||||
{
|
||||
label: `运输量(${unit})`,
|
||||
value: isByWeight ? displayTransportQty.value : '—'
|
||||
},
|
||||
{
|
||||
label: `运输单价(元/${unit})`,
|
||||
value: isByWeight ? displayFreightPrice.value : '—',
|
||||
money: isByWeight
|
||||
},
|
||||
{
|
||||
label: '运费金额(元)',
|
||||
value: hasFreight ? displayFreightAmount.value : '—',
|
||||
money: hasFreight
|
||||
},
|
||||
{ label: '是否开票', value: hasFreight ? displayHasInvoiceLabel.value : '—' }
|
||||
]
|
||||
if (plan.value?.hasFreight !== 1) return fields
|
||||
|
||||
fields.push(
|
||||
{ label: '运输公里数', value: displayDistanceKm.value },
|
||||
{ label: '运费计算方式', value: getPlFreightTypeLabel(displayFreightType.value) }
|
||||
)
|
||||
if (displayFreightType.value === 'by_weight') {
|
||||
fields.push(
|
||||
{ label: '运输量(吨)', value: displayTransportQty.value },
|
||||
{ label: '运输单价(元/吨)', value: displayFreightPrice.value, money: true }
|
||||
)
|
||||
}
|
||||
fields.push(
|
||||
{ label: '运费金额(元)', value: displayFreightAmount.value, money: true },
|
||||
{ label: '是否开票', value: getPlHasInvoiceLabelFromSettlement(freightSettlement.value) }
|
||||
)
|
||||
return fields
|
||||
})
|
||||
|
||||
const driverFeedbackViewFields = computed<ViewField[]>(() => {
|
||||
const fields: ViewField[] = [{ label: '回传时间', value: displayFeedbackTime.value }]
|
||||
if (transport.value?.feedbackQty != null) {
|
||||
fields.push({ label: '回传数量', value: transport.value.feedbackQty })
|
||||
}
|
||||
fields.push({ label: '回传备注', value: transport.value?.feedbackRemark, span: 'full' })
|
||||
return fields
|
||||
})
|
||||
|
||||
const contractBasicViewFields = computed<ViewField[]>(() => {
|
||||
@@ -712,7 +829,11 @@
|
||||
if (props.planId) return loadDetail(props.planId)
|
||||
}
|
||||
|
||||
defineExpose({ reload })
|
||||
function setActiveTab(name: string) {
|
||||
activeTab.value = name
|
||||
}
|
||||
|
||||
defineExpose({ reload, setActiveTab })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -785,6 +906,32 @@
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
.pl-plan-arrange-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pl-plan-arrange-links-hint {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.pl-arrange-link-row {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
&--readonly {
|
||||
grid-template-columns: 72px 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-arrange-link-label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.pl-plan-view-remark {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
@@ -931,43 +1078,52 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.pl-plan-feedback-attachments {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
.pl-plan-feedback-region .view-subtable-wrap {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.pl-plan-feedback-attachments__label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
.pl-plan-feedback-region .view-subtable-wrap :deep(.el-table.view-subtable) {
|
||||
--el-table-header-bg-color: #f8fafc;
|
||||
--el-table-header-text-color: #1e293b;
|
||||
--el-table-border-color: #e2e8f0;
|
||||
width: 100%;
|
||||
|
||||
.pl-plan-cert-block {
|
||||
padding: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
.el-table__header th.el-table__cell,
|
||||
.el-table__body td.el-table__cell {
|
||||
font-size: 14px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-top: 10px;
|
||||
.el-table__header th.el-table__cell .cell {
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.el-table__body td.el-table__cell .cell {
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-cert-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
.pl-plan-feedback-table__attachment {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
:deep(.attachment-upload) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
:deep(.attachment-list) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-cert-name {
|
||||
.pl-plan-feedback-table__empty-cell {
|
||||
color: var(--mint-text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.money-display {
|
||||
@@ -986,38 +1142,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-site-feedback-block {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
|
||||
& + & {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.pl-plan-site-feedback-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.pl-plan-site-feedback-empty {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pl-plan-site-feedback-batch {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
.pl-plan-site-feedback-attachments {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pl-plan-dibang-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -111,18 +111,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="innerVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Close, RefreshLeft, Search, Select } from '@element-plus/icons-vue'
|
||||
import { RefreshLeft, Search, Select } from '@element-plus/icons-vue'
|
||||
import { transportAddressApi, type TransportAddress } from '@/api/pl/index'
|
||||
import ListPagination from '@/components/ListPagination/index.vue'
|
||||
import { pickBxPageResult } from '@/utils/http/bx'
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
<template>
|
||||
<div class="vehicle-cert-part-tabs">
|
||||
<el-tabs v-if="hasTrailer" v-model="activePart" class="vehicle-cert-part-tabs__tabs">
|
||||
<PlCertFormList
|
||||
v-if="layout === 'table'"
|
||||
ref="unifiedCertFormRef"
|
||||
v-model="certList"
|
||||
:title="title"
|
||||
:upload-folder="uploadFolder"
|
||||
:cert-name-options="certNameOptions"
|
||||
layout="table"
|
||||
show-vehicle-part-column
|
||||
:license-plate="licensePlate"
|
||||
:trailer-no="trailerNo"
|
||||
/>
|
||||
|
||||
<el-tabs
|
||||
v-else-if="hasTrailer"
|
||||
v-model="activePart"
|
||||
class="vehicle-cert-part-tabs__tabs"
|
||||
>
|
||||
<el-tab-pane :label="headTabLabel" name="head">
|
||||
<PlCertFormList
|
||||
ref="headCertFormRef"
|
||||
@@ -8,6 +25,7 @@
|
||||
:title="headTabLabel"
|
||||
:upload-folder="uploadFolder"
|
||||
:cert-name-options="certNameOptions"
|
||||
:layout="layout"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="trailerTabLabel" name="trailer">
|
||||
@@ -17,6 +35,7 @@
|
||||
:title="trailerTabLabel"
|
||||
:upload-folder="uploadFolder"
|
||||
:cert-name-options="certNameOptions"
|
||||
:layout="layout"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
@@ -28,6 +47,7 @@
|
||||
:title="title"
|
||||
:upload-folder="uploadFolder"
|
||||
:cert-name-options="certNameOptions"
|
||||
layout="card"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -52,18 +72,21 @@ const props = withDefaults(
|
||||
uploadFolder: string
|
||||
certNameOptions?: string[]
|
||||
title?: string
|
||||
layout?: 'card' | 'table'
|
||||
}>(),
|
||||
{
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
certNameOptions: () => [],
|
||||
title: '车辆证书'
|
||||
title: '车辆证书',
|
||||
layout: 'card'
|
||||
}
|
||||
)
|
||||
|
||||
const certList = defineModel<CertRowWithKey[]>('modelValue', { default: () => [] })
|
||||
|
||||
const activePart = ref(PL_VEHICLE_CERT_PART_HEAD)
|
||||
const unifiedCertFormRef = ref<CertFormListRef>()
|
||||
const headCertFormRef = ref<CertFormListRef>()
|
||||
const trailerCertFormRef = ref<CertFormListRef>()
|
||||
|
||||
@@ -102,6 +125,7 @@ watch(hasTrailer, value => {
|
||||
[],
|
||||
false
|
||||
)
|
||||
unifiedCertFormRef.value?.clearUploadRefs()
|
||||
trailerCertFormRef.value?.clearUploadRefs()
|
||||
return
|
||||
}
|
||||
@@ -111,12 +135,16 @@ watch(hasTrailer, value => {
|
||||
})
|
||||
|
||||
function hasUploading() {
|
||||
if (props.layout === 'table') {
|
||||
return Boolean(unifiedCertFormRef.value?.hasUploading?.())
|
||||
}
|
||||
return Boolean(
|
||||
headCertFormRef.value?.hasUploading?.() || trailerCertFormRef.value?.hasUploading?.()
|
||||
)
|
||||
}
|
||||
|
||||
function clearUploadRefs() {
|
||||
unifiedCertFormRef.value?.clearUploadRefs()
|
||||
headCertFormRef.value?.clearUploadRefs()
|
||||
trailerCertFormRef.value?.clearUploadRefs()
|
||||
}
|
||||
|
||||
@@ -1,23 +1,52 @@
|
||||
<template>
|
||||
<div class="vehicle-cert-part-view">
|
||||
<el-tabs v-if="hasTrailer" v-model="activePart" class="vehicle-cert-part-view__tabs">
|
||||
<el-tab-pane :label="headTabLabel" name="head">
|
||||
<PlCertViewList :cert-list="headCertList" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="trailerTabLabel" name="trailer">
|
||||
<PlCertViewList :cert-list="trailerCertList" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<PlCertViewList
|
||||
v-if="certLayout === 'table'"
|
||||
:cert-list="scopedCertList"
|
||||
:table-mode="tableMode"
|
||||
layout="table"
|
||||
:show-vehicle-part-column="hasTrailer"
|
||||
:head-label="headTabLabel"
|
||||
:trailer-label="trailerTabLabel"
|
||||
/>
|
||||
|
||||
<PlCertViewList v-else :cert-list="headCertList" />
|
||||
<template v-else>
|
||||
<el-tabs
|
||||
v-if="hasTrailer"
|
||||
:key="tabsInstanceKey"
|
||||
v-model="activePart"
|
||||
class="vehicle-cert-part-view__tabs"
|
||||
>
|
||||
<el-tab-pane :label="headTabLabelWithHint" :name="headPartName">
|
||||
<PlCertViewList :cert-list="headCertList" :table-mode="tableMode" :layout="certLayout" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="trailerTabLabelWithHint" :name="trailerPartName">
|
||||
<PlCertViewList
|
||||
:cert-list="trailerCertList"
|
||||
:table-mode="tableMode"
|
||||
:layout="certLayout"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<PlCertViewList
|
||||
v-else
|
||||
:cert-list="headCertList"
|
||||
:table-mode="tableMode"
|
||||
:layout="certLayout"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PlCertViewList from '@/components/business/pl-cert-view-list/index.vue'
|
||||
import type { CertRead } from '@/api/pl'
|
||||
import type { ViewTableMode } from '@/components/business/system-view-dialog/types'
|
||||
import {
|
||||
PL_VEHICLE_CERT_PART_HEAD,
|
||||
PL_VEHICLE_CERT_PART_TRAILER,
|
||||
getSelfVehiclePartCertMissingSuffix,
|
||||
splitVehicleCertsByPart
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
|
||||
@@ -26,22 +55,69 @@ const props = withDefaults(
|
||||
certList?: CertRead[]
|
||||
licensePlate?: string
|
||||
trailerNo?: string
|
||||
vehicleId?: string
|
||||
checkSelfVehicleCerts?: boolean
|
||||
tableMode?: ViewTableMode
|
||||
certLayout?: 'card' | 'table'
|
||||
}>(),
|
||||
{
|
||||
certList: () => [],
|
||||
licensePlate: '',
|
||||
trailerNo: ''
|
||||
trailerNo: '',
|
||||
vehicleId: '',
|
||||
checkSelfVehicleCerts: false,
|
||||
tableMode: 'triple',
|
||||
certLayout: 'card'
|
||||
}
|
||||
)
|
||||
|
||||
const activePart = ref(PL_VEHICLE_CERT_PART_HEAD)
|
||||
const activePart = ref('')
|
||||
|
||||
const tabsInstanceKey = computed(
|
||||
() => `${props.vehicleId || props.licensePlate || 'vehicle'}-${props.trailerNo || 'head'}`
|
||||
)
|
||||
|
||||
const headPartName = computed(
|
||||
() => `${PL_VEHICLE_CERT_PART_HEAD}-${props.vehicleId || props.licensePlate || 'vehicle'}`
|
||||
)
|
||||
const trailerPartName = computed(
|
||||
() =>
|
||||
`${PL_VEHICLE_CERT_PART_TRAILER}-${props.vehicleId || props.trailerNo || props.licensePlate || 'vehicle'}`
|
||||
)
|
||||
|
||||
const hasTrailer = computed(() => Boolean(props.trailerNo?.trim()))
|
||||
const headTabLabel = computed(() => props.licensePlate?.trim() || '主车')
|
||||
const trailerTabLabel = computed(() => props.trailerNo?.trim() || '挂车')
|
||||
|
||||
const headCertList = computed(() => splitVehicleCertsByPart(props.certList).headCerts)
|
||||
const trailerCertList = computed(() => splitVehicleCertsByPart(props.certList).trailerCerts)
|
||||
const scopedCertList = computed(() => {
|
||||
const list = props.certList || []
|
||||
if (!props.vehicleId) return list
|
||||
return list.filter(cert => !cert.ownerId || cert.ownerId === props.vehicleId)
|
||||
})
|
||||
|
||||
const headCertList = computed(() => splitVehicleCertsByPart(scopedCertList.value).headCerts)
|
||||
const trailerCertList = computed(() => splitVehicleCertsByPart(scopedCertList.value).trailerCerts)
|
||||
|
||||
const headTabLabelWithHint = computed(() => {
|
||||
if (!props.checkSelfVehicleCerts) return headTabLabel.value
|
||||
return headTabLabel.value + getSelfVehiclePartCertMissingSuffix(headCertList.value, PL_VEHICLE_CERT_PART_HEAD)
|
||||
})
|
||||
|
||||
const trailerTabLabelWithHint = computed(() => {
|
||||
if (!props.checkSelfVehicleCerts) return trailerTabLabel.value
|
||||
return (
|
||||
trailerTabLabel.value +
|
||||
getSelfVehiclePartCertMissingSuffix(trailerCertList.value, PL_VEHICLE_CERT_PART_TRAILER)
|
||||
)
|
||||
})
|
||||
|
||||
watch(
|
||||
tabsInstanceKey,
|
||||
() => {
|
||||
activePart.value = headPartName.value
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<template>
|
||||
<el-tabs v-model="activeTab" class="vehicle-detail-tabs">
|
||||
<el-tab-pane label="车辆证书" name="vehicle-cert">
|
||||
<el-tabs :key="tabsInstanceKey" v-model="activeTab" class="vehicle-detail-tabs">
|
||||
<el-tab-pane :label="vehicleCertTabLabel" :name="vehicleCertTabName">
|
||||
<PlVehicleCertPartView
|
||||
:key="`${vehicleId || licensePlate}-cert`"
|
||||
:cert-list="certList"
|
||||
:license-plate="licensePlate"
|
||||
:trailer-no="trailerNo"
|
||||
:vehicle-id="vehicleId"
|
||||
:check-self-vehicle-certs="checkSelfVehicleCerts"
|
||||
:table-mode="tableMode"
|
||||
:cert-layout="certLayout"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
@@ -13,7 +18,12 @@
|
||||
:label="getPersonTabLabel(person)"
|
||||
:name="getPersonKey(person, index)"
|
||||
>
|
||||
<PersonnelViewPanel :person="person" />
|
||||
<PersonnelViewPanel
|
||||
:person="person"
|
||||
:check-self-vehicle-certs="checkSelfVehicleCerts"
|
||||
:table-mode="tableMode"
|
||||
:cert-layout="certLayout"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
@@ -23,26 +33,57 @@ import PlVehicleCertPartView from '@/components/business/pl-vehicle-cert-part-vi
|
||||
import PersonnelViewPanel from '@/components/business/pl-vehicle-personnel-view-list/PersonnelViewPanel.vue'
|
||||
import type { CertRead } from '@/api/pl'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import type { ViewTableMode } from '@/components/business/system-view-dialog/types'
|
||||
import { getSelfVehicleCertMissingSuffix } from '@/utils/pl-vehicle-cert'
|
||||
import { getPersonnelCertMissingSuffix } from '@/utils/pl-personnel-cert'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
certList?: CertRead[]
|
||||
licensePlate?: string
|
||||
trailerNo?: string
|
||||
vehicleId?: string
|
||||
personnelList?: VehiclePersonnelViewRow[]
|
||||
checkSelfVehicleCerts?: boolean
|
||||
tableMode?: ViewTableMode
|
||||
certLayout?: 'card' | 'table'
|
||||
}>(),
|
||||
{
|
||||
certList: () => [],
|
||||
licensePlate: '',
|
||||
trailerNo: '',
|
||||
personnelList: () => []
|
||||
vehicleId: '',
|
||||
personnelList: () => [],
|
||||
checkSelfVehicleCerts: false,
|
||||
tableMode: 'triple',
|
||||
certLayout: 'card'
|
||||
}
|
||||
)
|
||||
|
||||
const activeTab = ref('vehicle-cert')
|
||||
|
||||
const vehicleCertTabName = computed(
|
||||
() => `vehicle-cert-${props.vehicleId || props.licensePlate || 'default'}`
|
||||
)
|
||||
|
||||
const tabsInstanceKey = computed(
|
||||
() => `${props.vehicleId || props.licensePlate || 'vehicle'}-${props.trailerNo || 'none'}`
|
||||
)
|
||||
|
||||
const vehicleCertTabLabel = computed(() => {
|
||||
const label = '车辆证书'
|
||||
if (!props.checkSelfVehicleCerts) return label
|
||||
return (
|
||||
label +
|
||||
getSelfVehicleCertMissingSuffix({
|
||||
certList: props.certList,
|
||||
trailerNo: props.trailerNo
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
function getPersonKey(person: VehiclePersonnelViewRow, index: number) {
|
||||
return `person-${person.personnelId || `${person.personnelName}-${index}`}`
|
||||
return `person-${props.vehicleId || props.licensePlate || 'vehicle'}-${person.personnelId || `${person.personnelName}-${index}`}`
|
||||
}
|
||||
|
||||
function getRoleLabel(role: string) {
|
||||
@@ -56,23 +97,31 @@ function getRoleLabel(role: string) {
|
||||
function getPersonTabLabel(person: VehiclePersonnelViewRow) {
|
||||
const name = person.personnelName || '—'
|
||||
const role = getRoleLabel(person.role)
|
||||
return `${name}(${role})`
|
||||
const suffix =
|
||||
props.checkSelfVehicleCerts && person.role
|
||||
? getPersonnelCertMissingSuffix(person.certList || [], person.role as 'driver' | 'escort')
|
||||
: ''
|
||||
return `${name}(${role})${suffix}`
|
||||
}
|
||||
|
||||
watch(
|
||||
tabsInstanceKey,
|
||||
() => {
|
||||
activeTab.value = vehicleCertTabName.value
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.personnelList,
|
||||
list => {
|
||||
if (!list.length) {
|
||||
activeTab.value = 'vehicle-cert'
|
||||
return
|
||||
}
|
||||
if (activeTab.value === 'vehicle-cert') return
|
||||
if (activeTab.value === vehicleCertTabName.value) return
|
||||
const keys = list.map((person, index) => getPersonKey(person, index))
|
||||
if (!keys.includes(activeTab.value)) {
|
||||
activeTab.value = 'vehicle-cert'
|
||||
activeTab.value = vehicleCertTabName.value
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
{ deep: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
+25
-6
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="personnel-view-panel">
|
||||
<h4 v-if="showTitle" class="personnel-view-card-title">
|
||||
{{ person.personnelName || '—' }}
|
||||
{{ person.personnelName || '—' }}{{ personnelCertMissingSuffix }}
|
||||
<el-tag :type="getRoleTagType(person.role)" size="small">
|
||||
{{ getRoleLabel(person.role) }}
|
||||
</el-tag>
|
||||
@@ -10,11 +10,15 @@
|
||||
</el-tag>
|
||||
</h4>
|
||||
|
||||
<ViewKvTable :fields="personnelViewFields" mode="triple" />
|
||||
<ViewKvTable :fields="personnelViewFields" :mode="tableMode" />
|
||||
|
||||
<div class="personnel-certs-block">
|
||||
<p class="personnel-certs-title">人员证书</p>
|
||||
<PlCertViewList :cert-list="person.certList || []" />
|
||||
<PlCertViewList
|
||||
:cert-list="person.certList || []"
|
||||
:table-mode="tableMode"
|
||||
:layout="certLayout"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,8 +26,9 @@
|
||||
<script setup lang="ts">
|
||||
import PlCertViewList from '@/components/business/pl-cert-view-list/index.vue'
|
||||
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import type { ViewField, ViewTableMode } from '@/components/business/system-view-dialog/types'
|
||||
import type { CertRead, VehiclePersonnelRow } from '@/api/pl'
|
||||
import { getPersonnelCertMissingSuffix } from '@/utils/pl-personnel-cert'
|
||||
|
||||
type VehiclePersonnelViewRow = VehiclePersonnelRow & { certList?: CertRead[] }
|
||||
|
||||
@@ -31,12 +36,26 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
person: VehiclePersonnelViewRow
|
||||
showTitle?: boolean
|
||||
checkSelfVehicleCerts?: boolean
|
||||
tableMode?: ViewTableMode
|
||||
certLayout?: 'card' | 'table'
|
||||
}>(),
|
||||
{
|
||||
showTitle: false
|
||||
showTitle: false,
|
||||
checkSelfVehicleCerts: false,
|
||||
tableMode: 'triple',
|
||||
certLayout: 'card'
|
||||
}
|
||||
)
|
||||
|
||||
const personnelCertMissingSuffix = computed(() => {
|
||||
if (!props.checkSelfVehicleCerts || !props.person.role) return ''
|
||||
return getPersonnelCertMissingSuffix(
|
||||
props.person.certList || [],
|
||||
props.person.role as 'driver' | 'escort'
|
||||
)
|
||||
})
|
||||
|
||||
const personnelViewFields = computed<ViewField[]>(() => {
|
||||
const person = props.person
|
||||
const fields: ViewField[] = [
|
||||
@@ -58,7 +77,7 @@ const personnelViewFields = computed<ViewField[]>(() => {
|
||||
})
|
||||
}
|
||||
|
||||
fields.push({ label: '备注', value: person.remark || '—', span: 'full' })
|
||||
fields.push({ label: '备注', value: person.remark || '—', long: false })
|
||||
|
||||
return fields
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
:label="getTabLabel(person)"
|
||||
:name="getPersonKey(person, index)"
|
||||
>
|
||||
<PersonnelViewPanel :person="person" />
|
||||
<PersonnelViewPanel :person="person" :check-self-vehicle-certs="checkSelfVehicleCerts" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
:key="getPersonKey(person, index)"
|
||||
class="personnel-view-card"
|
||||
>
|
||||
<PersonnelViewPanel :person="person" show-title />
|
||||
<PersonnelViewPanel :person="person" show-title :check-self-vehicle-certs="checkSelfVehicleCerts" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,6 +32,7 @@
|
||||
<script setup lang="ts">
|
||||
import PersonnelViewPanel from './PersonnelViewPanel.vue'
|
||||
import type { CertRead, VehiclePersonnelRow } from '@/api/pl'
|
||||
import { getPersonnelCertMissingSuffix } from '@/utils/pl-personnel-cert'
|
||||
|
||||
export interface VehiclePersonnelViewRow extends VehiclePersonnelRow {
|
||||
certList?: CertRead[]
|
||||
@@ -41,10 +42,12 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
personnelList?: VehiclePersonnelViewRow[]
|
||||
layout?: 'list' | 'tabs'
|
||||
checkSelfVehicleCerts?: boolean
|
||||
}>(),
|
||||
{
|
||||
personnelList: () => [],
|
||||
layout: 'list'
|
||||
layout: 'list',
|
||||
checkSelfVehicleCerts: false
|
||||
}
|
||||
)
|
||||
|
||||
@@ -65,7 +68,11 @@ function getRoleLabel(role: string) {
|
||||
function getTabLabel(person: VehiclePersonnelViewRow) {
|
||||
const name = person.personnelName || '—'
|
||||
const role = getRoleLabel(person.role)
|
||||
return `${name}(${role})`
|
||||
const suffix =
|
||||
props.checkSelfVehicleCerts && person.role
|
||||
? getPersonnelCertMissingSuffix(person.certList || [], person.role as 'driver' | 'escort')
|
||||
: ''
|
||||
return `${name}(${role})${suffix}`
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
<template #vehicleDetailTabs>
|
||||
<PlVehicleCertPersonnelTabs
|
||||
:cert-list="detail?.certList || []"
|
||||
:license-plate="detail?.licensePlate"
|
||||
:trailer-no="detail?.trailerNo"
|
||||
:vehicle-id="detail?.id"
|
||||
:personnel-list="personnelList"
|
||||
:check-self-vehicle-certs="detail?.vehicleType === 'self'"
|
||||
cert-layout="table"
|
||||
/>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
|
||||
@@ -19,13 +19,19 @@
|
||||
<div v-show="!collapsible || expanded" class="vehicle-view-panel-body">
|
||||
<section class="vehicle-view-section">
|
||||
<h4 v-if="!collapsible" class="vehicle-view-section-title">车辆信息</h4>
|
||||
<ViewKvTable :fields="vehicleViewFields" mode="triple" />
|
||||
<ViewKvTable :fields="vehicleViewFields" :mode="tableMode" />
|
||||
</section>
|
||||
|
||||
<section class="vehicle-view-section">
|
||||
<PlVehicleCertPersonnelTabs
|
||||
:key="detail.id"
|
||||
:cert-list="detail.certList || []"
|
||||
:license-plate="detail.licensePlate"
|
||||
:trailer-no="detail.trailerNo"
|
||||
:vehicle-id="detail.id"
|
||||
:personnel-list="personnelList"
|
||||
:table-mode="tableMode"
|
||||
:cert-layout="certLayout"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
@@ -36,7 +42,7 @@
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import PlVehicleCertPersonnelTabs from '@/components/business/pl-vehicle-cert-personnel-tabs/index.vue'
|
||||
import ViewKvTable from '@/components/business/system-view-dialog/ViewKvTable.vue'
|
||||
import type { ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import type { ViewField, ViewTableMode } from '@/components/business/system-view-dialog/types'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import type { VehicleDetail } from '@/api/pl'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
@@ -53,11 +59,15 @@ const props = withDefaults(
|
||||
personnelList?: VehiclePersonnelViewRow[]
|
||||
collapsible?: boolean
|
||||
defaultExpanded?: boolean
|
||||
tableMode?: ViewTableMode
|
||||
certLayout?: 'card' | 'table'
|
||||
}>(),
|
||||
{
|
||||
personnelList: () => [],
|
||||
collapsible: false,
|
||||
defaultExpanded: false
|
||||
defaultExpanded: false,
|
||||
tableMode: 'triple',
|
||||
certLayout: 'card'
|
||||
}
|
||||
)
|
||||
|
||||
@@ -78,11 +88,12 @@ const vehicleViewFields = computed<ViewField[]>(() => {
|
||||
{
|
||||
label: '车辆状态',
|
||||
value: getPlVehicleStatusLabel(d.vehicleStatus),
|
||||
tagType: getPlVehicleStatusTagType(d.vehicleStatus)
|
||||
tagType: getPlVehicleStatusTagType(d.vehicleStatus),
|
||||
long: false
|
||||
},
|
||||
{ label: '关联人员摘要', value: d.personnelSummary || '—', span: true },
|
||||
{ label: '创建时间', value: formatDateTime(d.createTime) },
|
||||
{ label: '备注', value: d.remark || '—', span: 'full' }
|
||||
{ label: '创建时间', value: formatDateTime(d.createTime), long: false },
|
||||
{ label: '关联人员摘要', value: d.personnelSummary || '—', long: false },
|
||||
{ label: '备注', value: d.remark || '—', long: false }
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -258,12 +258,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="emit('update:modelValue', false)">
|
||||
<el-icon><Close /></el-icon>
|
||||
关闭
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<el-dialog
|
||||
v-model="previewVisible"
|
||||
@@ -299,7 +293,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { Close, Document, Money, Picture, Setting, VideoPlay } from '@element-plus/icons-vue'
|
||||
import { Document, Money, Picture, Setting, VideoPlay } from '@element-plus/icons-vue'
|
||||
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
|
||||
import {
|
||||
fetchProductDetail,
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div v-if="items.length" class="view-highlights">
|
||||
<span
|
||||
v-for="(item, index) in items"
|
||||
:key="item.label"
|
||||
class="view-highlight-item"
|
||||
:class="{
|
||||
'is-emphasis': item.emphasis,
|
||||
'is-warn': item.warn
|
||||
}"
|
||||
>
|
||||
<span class="view-highlight-sep" v-if="index > 0">·</span>
|
||||
<span class="view-highlight-label">{{ item.label }}</span>
|
||||
<span class="view-highlight-value">
|
||||
<el-tag v-if="item.tagType" :type="item.tagType" size="small">{{
|
||||
formatViewValue(item.value)
|
||||
}}</el-tag>
|
||||
<span v-else :class="{ 'money-display': item.money }">{{
|
||||
formatViewValue(item.value)
|
||||
}}</span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ViewHighlight } from './types'
|
||||
import { formatViewValue } from './types'
|
||||
|
||||
defineProps<{
|
||||
items: ViewHighlight[]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.view-highlights {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px 0;
|
||||
padding: 0 16px 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.view-highlight-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.view-highlight-sep {
|
||||
margin: 0 10px;
|
||||
color: #cbd5e1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.view-highlight-label {
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.view-highlight-value {
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e293b);
|
||||
}
|
||||
|
||||
.view-highlight-item.is-emphasis .view-highlight-value {
|
||||
color: var(--primary-dark, #059669);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.view-highlight-item.is-warn .view-highlight-value {
|
||||
color: #d97706;
|
||||
}
|
||||
</style>
|
||||
@@ -28,7 +28,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<th>{{ row.left.label }}</th>
|
||||
<td>
|
||||
<td :colspan="row.right ? undefined : 3">
|
||||
<ViewKvTableCell :field="row.left" />
|
||||
</td>
|
||||
<template v-if="row.right">
|
||||
@@ -37,10 +37,6 @@
|
||||
<ViewKvTableCell :field="row.right" />
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</template>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -57,22 +53,22 @@
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="slot in 3" :key="slot">
|
||||
<template v-if="row.fields[slot - 1]">
|
||||
<th>{{ row.fields[slot - 1].label }}</th>
|
||||
<td>
|
||||
<ViewKvTableCell :field="row.fields[slot - 1]" />
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</template>
|
||||
<template v-for="(field, fi) in row.fields" :key="field.label">
|
||||
<th>{{ field.label }}</th>
|
||||
<td
|
||||
:colspan="
|
||||
fi === row.fields.length - 1 && row.fields.length < 3
|
||||
? (3 - row.fields.length) * 2 + 1
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<ViewKvTableCell :field="field" />
|
||||
</td>
|
||||
</template>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-else-if="mode === 'quad'">
|
||||
<tr
|
||||
v-for="(row, idx) in quadRows"
|
||||
:key="quadRowKey(row, idx)"
|
||||
@@ -85,17 +81,45 @@
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="slot in 4" :key="slot">
|
||||
<template v-if="row.fields[slot - 1]">
|
||||
<th>{{ row.fields[slot - 1].label }}</th>
|
||||
<td>
|
||||
<ViewKvTableCell :field="row.fields[slot - 1]" />
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<th></th>
|
||||
<td></td>
|
||||
</template>
|
||||
<template v-for="(field, fi) in row.fields" :key="field.label">
|
||||
<th>{{ field.label }}</th>
|
||||
<td
|
||||
:colspan="
|
||||
fi === row.fields.length - 1 && row.fields.length < 4
|
||||
? (4 - row.fields.length) * 2 + 1
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<ViewKvTableCell :field="field" />
|
||||
</td>
|
||||
</template>
|
||||
</template>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-else>
|
||||
<tr
|
||||
v-for="(row, idx) in quintRows"
|
||||
:key="quintRowKey(row, idx)"
|
||||
:class="{ 'row-long': row.type === 'long' }"
|
||||
>
|
||||
<template v-if="row.type === 'long'">
|
||||
<th>{{ row.field.label }}</th>
|
||||
<td colspan="9">
|
||||
<ViewKvTableCell :field="row.field" />
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="(field, fi) in row.fields" :key="field.label">
|
||||
<th>{{ field.label }}</th>
|
||||
<td
|
||||
:colspan="
|
||||
fi === row.fields.length - 1 && row.fields.length < 5
|
||||
? (5 - row.fields.length) * 2 + 1
|
||||
: undefined
|
||||
"
|
||||
>
|
||||
<ViewKvTableCell :field="field" />
|
||||
</td>
|
||||
</template>
|
||||
</template>
|
||||
</tr>
|
||||
@@ -113,11 +137,13 @@
|
||||
import {
|
||||
buildViewKvTableDualRows,
|
||||
buildViewKvTableQuadRows,
|
||||
buildViewKvTableQuintRows,
|
||||
buildViewKvTableTripleRows,
|
||||
getViewFieldDisplayText,
|
||||
isViewFieldFullSpan,
|
||||
isViewFieldLong,
|
||||
type ViewKvTableQuadRow,
|
||||
type ViewKvTableQuintRow,
|
||||
type ViewKvTableRow,
|
||||
type ViewKvTableTripleRow
|
||||
} from '@/utils/view-field-layout'
|
||||
@@ -125,8 +151,8 @@
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
fields: ViewField[]
|
||||
/** pair:一行一组;dual:一行两组(四列);triple:一行三组(六列);quad:一行四组(八列) */
|
||||
mode?: 'pair' | 'dual' | 'triple' | 'quad'
|
||||
/** pair:一行一组;dual:一行两组(四列);triple:一行三组(六列);quad:一行四组(八列);quint:一行五组(十列) */
|
||||
mode?: 'pair' | 'dual' | 'triple' | 'quad' | 'quint'
|
||||
}>(),
|
||||
{
|
||||
mode: 'pair'
|
||||
@@ -136,6 +162,7 @@
|
||||
const dualRows = computed(() => buildViewKvTableDualRows(props.fields, formatViewValue))
|
||||
const tripleRows = computed(() => buildViewKvTableTripleRows(props.fields, formatViewValue))
|
||||
const quadRows = computed(() => buildViewKvTableQuadRows(props.fields, formatViewValue))
|
||||
const quintRows = computed(() => buildViewKvTableQuintRows(props.fields, formatViewValue))
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'field-view', field: ViewField): void
|
||||
@@ -162,6 +189,11 @@
|
||||
if (row.type === 'long') return row.field.label
|
||||
return row.fields.map((f) => f.label).join('-') || String(idx)
|
||||
}
|
||||
|
||||
function quintRowKey(row: ViewKvTableQuintRow, idx: number) {
|
||||
if (row.type === 'long') return row.field.label
|
||||
return row.fields.map((f) => f.label).join('-') || String(idx)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -261,4 +293,20 @@
|
||||
.view-kv-table--quad tr.row-long td {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.view-kv-table--quint th {
|
||||
width: 6.5%;
|
||||
}
|
||||
|
||||
.view-kv-table--quint td {
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
.view-kv-table--quint tr.row-long th {
|
||||
width: 6.5%;
|
||||
}
|
||||
|
||||
.view-kv-table--quint tr.row-long td {
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,17 @@
|
||||
<span v-else :class="{ 'money-display': field.money, 'muted-display': isMuted }">{{
|
||||
formatViewValue(field.value)
|
||||
}}</span>
|
||||
<el-button
|
||||
v-if="showMapBtn"
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
class="view-kv-field-link"
|
||||
title="在高德地图查看"
|
||||
@click="openAmapLocation"
|
||||
>
|
||||
<el-icon><Location /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="showViewBtn"
|
||||
link
|
||||
@@ -24,9 +35,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
import { View } from '@element-plus/icons-vue'
|
||||
import { Location, View } from '@element-plus/icons-vue'
|
||||
import type { ViewField } from './types'
|
||||
import { formatViewValue } from './types'
|
||||
import { buildAmapLocationUrl } from '@/utils/amap/navigation'
|
||||
|
||||
const VIEW_KV_FIELD_VIEW_KEY = 'viewKvFieldView'
|
||||
|
||||
@@ -48,9 +60,27 @@
|
||||
return true
|
||||
})
|
||||
|
||||
const amapLocationUrl = computed(() => {
|
||||
if (!props.field.showMapLink) return null
|
||||
return buildAmapLocationUrl({
|
||||
lng: props.field.mapLng,
|
||||
lat: props.field.mapLat,
|
||||
name: props.field.mapName,
|
||||
address: formatViewValue(props.field.value) === '—' ? undefined : String(props.field.value ?? '')
|
||||
})
|
||||
})
|
||||
|
||||
const showMapBtn = computed(() => Boolean(amapLocationUrl.value))
|
||||
|
||||
function handleView() {
|
||||
onFieldView?.(props.field)
|
||||
}
|
||||
|
||||
function openAmapLocation() {
|
||||
const url = amapLocationUrl.value
|
||||
if (!url) return
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="innerVisible"
|
||||
:title="title"
|
||||
:width="width"
|
||||
class="system-view-dialog"
|
||||
:class="[dialogClass, { 'system-view-dialog--compact': compact }]"
|
||||
:class="[
|
||||
dialogClass,
|
||||
{ 'system-view-dialog--compact': compact },
|
||||
{ 'system-view-dialog--dense': dense }
|
||||
]"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:show-close="false"
|
||||
:append-to-body="appendToBody"
|
||||
:modal-class="modalClass"
|
||||
style="backdrop-filter: none"
|
||||
@@ -34,7 +38,11 @@
|
||||
:key="sectionKey(section, si)"
|
||||
class="view-section"
|
||||
>
|
||||
<h3 v-if="section.title && section.titleStyle !== 'field'">{{ section.title }}</h3>
|
||||
<h3
|
||||
v-if="section.title && section.titleStyle !== 'field' && !(dense && section.hideTitleInDense)"
|
||||
>
|
||||
{{ section.title }}
|
||||
</h3>
|
||||
<div v-else-if="section.title && section.titleStyle === 'field'" class="view-field-label">
|
||||
{{ section.title }}
|
||||
</div>
|
||||
@@ -108,9 +116,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ViewHighlights v-if="highlights?.length" :items="highlights" />
|
||||
|
||||
<div :class="dense ? 'view-dense-body' : 'view-sections-stack'">
|
||||
<!-- sections -->
|
||||
<div v-for="(section, si) in sections" :key="sectionKey(section, si)" class="view-section">
|
||||
<h3 v-if="section.title && section.titleStyle !== 'field'">{{ section.title }}</h3>
|
||||
<div
|
||||
v-for="(section, si) in sections"
|
||||
:key="sectionKey(section, si)"
|
||||
class="view-section"
|
||||
:class="{ 'view-section--muted': dense && section.variant === 'muted' }"
|
||||
>
|
||||
<h3
|
||||
v-if="section.title && section.titleStyle !== 'field' && !(dense && section.hideTitleInDense)"
|
||||
>
|
||||
{{ section.title }}
|
||||
</h3>
|
||||
<div v-else-if="section.title && section.titleStyle === 'field'" class="view-field-label">
|
||||
{{ section.title }}
|
||||
</div>
|
||||
@@ -162,23 +182,20 @@
|
||||
<slot :name="section.slot" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="append" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="innerVisible = false">
|
||||
<el-icon><Close /></el-icon>关闭
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, useSlots, type Component } from 'vue'
|
||||
import { Close, Document } from '@element-plus/icons-vue'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import ViewKvGrid from './ViewKvGrid.vue'
|
||||
import ViewKvTable from './ViewKvTable.vue'
|
||||
import ViewHighlights from './ViewHighlights.vue'
|
||||
import {
|
||||
isIconComponent,
|
||||
type TagType,
|
||||
@@ -186,15 +203,26 @@
|
||||
type ViewFieldGroup,
|
||||
type ViewFieldLayout,
|
||||
type ViewGridColumns,
|
||||
type ViewHighlight,
|
||||
type ViewSection,
|
||||
type ViewTableMode
|
||||
} from './types'
|
||||
|
||||
export type { TagType, ViewField, ViewFieldGroup, ViewFieldLayout, ViewSection, ViewTableMode }
|
||||
export type {
|
||||
TagType,
|
||||
ViewField,
|
||||
ViewFieldGroup,
|
||||
ViewFieldLayout,
|
||||
ViewHighlight,
|
||||
ViewSection,
|
||||
ViewTableMode
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean
|
||||
/** 推荐:v-model:visible;兼容旧写法 v-model(modelValue) */
|
||||
visible?: boolean
|
||||
modelValue?: boolean
|
||||
title: string
|
||||
icon?: Component
|
||||
meta?: string
|
||||
@@ -215,6 +243,13 @@
|
||||
dialogClass?: string
|
||||
/** 紧凑布局:缩小头部/区块间距与内容区最大高度 */
|
||||
compact?: boolean
|
||||
/**
|
||||
* 聚拢布局:核心指标条 + 单面板分区,信息密度更高。
|
||||
* 各业务页仅需配置 highlights / sections,无需单独写样式。
|
||||
*/
|
||||
dense?: boolean
|
||||
/** dense 模式下 hero 下方的核心指标(2~6 项) */
|
||||
highlights?: ViewHighlight[]
|
||||
/** 详情异步加载中 */
|
||||
loading?: boolean
|
||||
/** default 单列滚动;split 左右分栏(需 split-right 插槽) */
|
||||
@@ -229,6 +264,8 @@
|
||||
modalClass: '',
|
||||
dialogClass: '',
|
||||
compact: false,
|
||||
dense: false,
|
||||
highlights: () => [],
|
||||
loading: false,
|
||||
layout: 'default',
|
||||
splitGridColumns: 1
|
||||
@@ -241,18 +278,24 @@
|
||||
() => props.layout === 'split' || Boolean(slots['split-right'])
|
||||
)
|
||||
|
||||
/** 紧凑模式降为 2 列,避免 4 列过密 */
|
||||
const effectiveGridColumns = computed<ViewGridColumns>(() =>
|
||||
props.compact ? 2 : props.gridColumns
|
||||
)
|
||||
/** 紧凑(非 dense)降为 2 列;聚拢模式保持 4 列密度 */
|
||||
const effectiveGridColumns = computed<ViewGridColumns>(() => {
|
||||
if (props.dense) return props.gridColumns
|
||||
if (props.compact) return 2
|
||||
return props.gridColumns
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', val: boolean): void
|
||||
(e: 'update:modelValue', val: boolean): void
|
||||
}>()
|
||||
|
||||
const innerVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (val) => emit('update:visible', val)
|
||||
get: () => props.visible ?? props.modelValue ?? false,
|
||||
set: (val) => {
|
||||
emit('update:visible', val)
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
})
|
||||
|
||||
/** 须为 @element-plus/icons-vue 组件;字符串或未传时使用默认 Document */
|
||||
@@ -272,8 +315,9 @@
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dialog-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 120px);
|
||||
|
||||
&--split {
|
||||
overflow: hidden;
|
||||
@@ -286,7 +330,6 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.view-split-left {
|
||||
@@ -464,10 +507,6 @@
|
||||
}
|
||||
|
||||
.system-view-dialog--compact {
|
||||
.dialog-scroll {
|
||||
max-height: min(68vh, 560px);
|
||||
}
|
||||
|
||||
.view-hero {
|
||||
gap: 12px;
|
||||
padding: 12px 18px;
|
||||
@@ -489,7 +528,7 @@
|
||||
}
|
||||
|
||||
.view-hero-meta {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.view-section {
|
||||
@@ -511,7 +550,7 @@
|
||||
|
||||
.view-field-label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.view-field-group--spaced {
|
||||
@@ -519,7 +558,93 @@
|
||||
}
|
||||
|
||||
.view-field-group .view-empty-hint {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.system-view-dialog--dense {
|
||||
:deep(.view-kv-grid) {
|
||||
gap: 10px 14px;
|
||||
}
|
||||
|
||||
:deep(.view-kv) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
align-items: stretch;
|
||||
|
||||
dt {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.view-kv-span-2) {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
:deep(.view-kv-span-full) {
|
||||
grid-column: 1 / -1;
|
||||
|
||||
dd {
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.view-kv-table-wrap) {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:deep(.view-kv-table) {
|
||||
border: none;
|
||||
|
||||
th {
|
||||
padding: 5px 8px;
|
||||
font-size: 13px;
|
||||
background: transparent;
|
||||
border-bottom: 1px solid #eef4f1;
|
||||
border-right: none;
|
||||
width: auto;
|
||||
min-width: 72px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 5px 10px 5px 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid #eef4f1;
|
||||
}
|
||||
|
||||
tr:last-child th,
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.view-field-label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.view-section-slot .view-empty-hint {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -531,14 +656,71 @@
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.system-view-dialog {
|
||||
.el-dialog__header {
|
||||
display: none;
|
||||
/* dense 布局间距:非 scoped,确保 append-to-body 时 hero 与信息区间距与 Demo 一致 */
|
||||
.system-view-dialog.system-view-dialog--dense,
|
||||
.el-dialog.system-view-dialog--dense {
|
||||
.view-hero {
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px 16px 8px;
|
||||
background: #fff !important;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.view-hero-icon {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.view-hero-main h2 {
|
||||
margin-bottom: 2px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.view-hero-meta {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.view-dense-body {
|
||||
margin: 0;
|
||||
padding: 16px 16px 12px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.view-dense-body > .view-section {
|
||||
padding: 0 !important;
|
||||
margin: 0;
|
||||
border-bottom: none !important;
|
||||
|
||||
&.view-section--muted {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed #e2ebe6;
|
||||
}
|
||||
|
||||
> h3 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.view-dense-body .view-field-label {
|
||||
margin: 10px 0 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.view-dense-body .view-section:first-child .view-field-label {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.system-view-dialog {
|
||||
.el-dialog__body {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
@@ -601,7 +783,7 @@
|
||||
}
|
||||
|
||||
.view-section-slot .view-empty-hint {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -14,17 +14,22 @@ export interface ViewField {
|
||||
span?: boolean | 'full'
|
||||
/** 显式长值,等同 span:true */
|
||||
long?: boolean
|
||||
/** 显式占满整行,等同 span:'full' */
|
||||
/** 显式占满整行,等同 span:'full';false 时禁止按超长文本自动占满 */
|
||||
fullSpan?: boolean
|
||||
tagType?: TagType
|
||||
money?: boolean
|
||||
/** 值后展示「查看」按钮,点击时 ViewKvTable 抛出 field-view */
|
||||
viewTarget?: 'customer' | 'opportunity' | 'quote'
|
||||
viewId?: string | number | null
|
||||
/** 为 true 时,值后展示地图图标跳转高德查看位置(须配合 mapLng/mapLat 或地址文案) */
|
||||
showMapLink?: boolean
|
||||
mapLng?: number | null
|
||||
mapLat?: number | null
|
||||
mapName?: string
|
||||
}
|
||||
|
||||
export type ViewFieldLayout = 'grid' | 'table'
|
||||
export type ViewTableMode = 'pair' | 'dual' | 'triple' | 'quad'
|
||||
export type ViewTableMode = 'pair' | 'dual' | 'triple' | 'quad' | 'quint'
|
||||
|
||||
export interface ViewFieldGroup {
|
||||
title?: string
|
||||
@@ -40,6 +45,10 @@ export interface ViewSection {
|
||||
title: string
|
||||
fields: ViewField[]
|
||||
fieldGroups?: ViewFieldGroup[]
|
||||
/** dense 模式下:muted = 底部分隔区(描述/备注等),默认 default */
|
||||
variant?: ViewSectionVariant
|
||||
/** dense 模式下隐藏区块大标题(信息并入单表) */
|
||||
hideTitleInDense?: boolean
|
||||
titleStyle?: 'section' | 'field'
|
||||
/** 默认 grid;table 时使用 ViewKvTable(如 triple = 一行三组 / 2 行 3 列) */
|
||||
fieldLayout?: ViewFieldLayout
|
||||
@@ -51,6 +60,19 @@ export interface ViewSection {
|
||||
|
||||
export type ViewGridColumns = 1 | 2 | 3 | 4
|
||||
|
||||
/** 查看弹窗顶部核心指标条(各业务页按场景配置 2~6 项) */
|
||||
export interface ViewHighlight {
|
||||
label: string
|
||||
value?: string | number | boolean | null | undefined
|
||||
money?: boolean
|
||||
/** 主指标:字号略大、保持主色强调 */
|
||||
emphasis?: boolean
|
||||
warn?: boolean
|
||||
tagType?: TagType
|
||||
}
|
||||
|
||||
export type ViewSectionVariant = 'default' | 'muted'
|
||||
|
||||
export function isIconComponent(val: unknown): val is Component {
|
||||
return val != null && (typeof val === 'object' || typeof val === 'function')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="dlg-view-section dlg-view-section-tabs">
|
||||
<el-tabs v-model="activeTab" class="dlg-view-flow-tabs">
|
||||
<el-tab-pane label="审批进度" name="flow">
|
||||
<div class="flow-scroll">
|
||||
<ApprovalFlowView :nodes="nodes" :loading="loading" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="审批历史" name="history">
|
||||
<div class="flow-scroll">
|
||||
<ApprovalHistoryView :items="history" :loading="loading" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import ApprovalFlowView from './ApprovalFlowView.vue'
|
||||
import ApprovalHistoryView from './ApprovalHistoryView.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
nodes: unknown[]
|
||||
history: unknown[]
|
||||
loading?: boolean
|
||||
}>(),
|
||||
{
|
||||
loading: false
|
||||
}
|
||||
)
|
||||
|
||||
const activeTab = ref('flow')
|
||||
|
||||
watch(
|
||||
() => [props.nodes, props.history],
|
||||
() => {
|
||||
activeTab.value = 'flow'
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -1,22 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { Refresh, Search } from '@element-plus/icons-vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
/** 是否与「展开更多条件」联动(收起/展开文案);无更多筛选项时可不传 */
|
||||
showMore?: boolean
|
||||
/** 为 true 时才显示「展开更多条件 / 收起更多条件」按钮 */
|
||||
hasExpandableFilters?: boolean
|
||||
loading?: boolean
|
||||
}>(),
|
||||
{
|
||||
showMore: false,
|
||||
hasExpandableFilters: false,
|
||||
}
|
||||
)
|
||||
defineProps<{
|
||||
loading?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleMore: []
|
||||
reset: []
|
||||
search: []
|
||||
}>()
|
||||
@@ -28,14 +17,7 @@ const emit = defineEmits<{
|
||||
<slot />
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<el-button
|
||||
v-if="hasExpandableFilters"
|
||||
link
|
||||
class="btn-link"
|
||||
@click="emit('toggleMore')"
|
||||
>
|
||||
{{ showMore ? '收起更多条件' : '展开更多条件' }}
|
||||
</el-button>
|
||||
<div class="filter-actions-start" />
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="filter-reset" :loading="loading" @click="emit('reset')">
|
||||
<el-icon class="ic"><Refresh /></el-icon>
|
||||
|
||||
@@ -8,9 +8,6 @@ import BaseTable from '../base/BaseTable.vue'
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
panelTitle: string
|
||||
showMore?: boolean
|
||||
/** 为 true 时显示「展开更多条件」;筛选项全部在主区域时不要传或传 false */
|
||||
hasExpandableFilters?: boolean
|
||||
selectedCount: number
|
||||
rows: any[]
|
||||
rangeText: string
|
||||
@@ -28,13 +25,10 @@ const props = withDefaults(
|
||||
{
|
||||
pageSizes: () => [10, 20, 30],
|
||||
showSummary: false,
|
||||
showMore: false,
|
||||
hasExpandableFilters: false,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleMore: []
|
||||
reset: []
|
||||
search: []
|
||||
'update:currentPage': [value: number]
|
||||
@@ -56,14 +50,7 @@ const pageSizeModel = computed({
|
||||
|
||||
<template>
|
||||
<DataTablePanel :title="panelTitle">
|
||||
<FilterBlock
|
||||
:show-more="showMore"
|
||||
:has-expandable-filters="hasExpandableFilters"
|
||||
:loading="loading"
|
||||
@toggle-more="emit('toggleMore')"
|
||||
@reset="emit('reset')"
|
||||
@search="emit('search')"
|
||||
>
|
||||
<FilterBlock :loading="loading" @reset="emit('reset')" @search="emit('search')">
|
||||
<slot name="filters" />
|
||||
</FilterBlock>
|
||||
|
||||
@@ -74,9 +61,9 @@ const pageSizeModel = computed({
|
||||
<div class="table-scroll" :class="tableScrollClass">
|
||||
<BaseTable
|
||||
:data="rows"
|
||||
:show-summary="showSummary"
|
||||
:summary-method="summaryMethod"
|
||||
:max-height="tableMaxHeight"
|
||||
:show-summary="showSummary"
|
||||
:summary-method="summaryMethod"
|
||||
:max-height="tableMaxHeight"
|
||||
@selection-change="(rows: any[]) => emit('selectionChange', rows)"
|
||||
>
|
||||
<slot name="table-columns" />
|
||||
|
||||
@@ -384,12 +384,6 @@
|
||||
* 监听窗口尺寸变化,自动处理移动端菜单
|
||||
*/
|
||||
watch(width, (newWidth) => {
|
||||
if (isPortalMenu.value) {
|
||||
settingStore.setMenuOpen(true)
|
||||
showMobileModal.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (newWidth < MOBILE_BREAKPOINT) {
|
||||
settingStore.setMenuOpen(false)
|
||||
if (!menuOpen.value) {
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
<!-- 一体化门户顶栏 -->
|
||||
<template>
|
||||
<header class="portal-header">
|
||||
<div class="portal-header-brand" @click="toHome">
|
||||
<ArtLogo class="portal-header-logo" src="/logo-sx.svg" :size="38" />
|
||||
<p class="portal-header-title">{{ AppConfig.systemInfo.name }}</p>
|
||||
<div class="portal-header-start">
|
||||
<div class="portal-header-brand" @click="toHome">
|
||||
<ArtLogo class="portal-header-logo" src="/logo-sx.svg" :size="38" />
|
||||
<p class="portal-header-title">{{ AppConfig.systemInfo.name }}</p>
|
||||
</div>
|
||||
|
||||
<div class="portal-header-nav">
|
||||
<ArtIconButton
|
||||
icon="fold"
|
||||
class="portal-header-icon-btn portal-header-fold-btn"
|
||||
@click="toggleSidebar"
|
||||
/>
|
||||
<ArtPortalModuleSelect />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="portal-header-nav">
|
||||
<ArtPortalModuleSelect />
|
||||
<ArtPortalSubNav class="portal-header-sub-nav" />
|
||||
<div class="portal-header-center" aria-hidden="false">
|
||||
<p v-if="pageTitle" class="portal-header-page-title">{{ pageTitle }}</p>
|
||||
</div>
|
||||
|
||||
<div class="portal-header-actions">
|
||||
<ArtPortalMobileMenu />
|
||||
<ArtIconButton
|
||||
v-if="shouldShowFullscreen"
|
||||
icon="fullscreen"
|
||||
:class="[
|
||||
'portal-header-icon-btn',
|
||||
'portal-header-icon-btn max-md:!hidden',
|
||||
!isFullscreen ? 'full-screen-btn' : 'exit-full-screen-btn'
|
||||
]"
|
||||
class="max-md:!hidden"
|
||||
@click="toggleFullScreen"
|
||||
/>
|
||||
<ArtUserMenu />
|
||||
@@ -29,19 +37,34 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import AppConfig from '@/config'
|
||||
import { useCommon } from '@/hooks/core/useCommon'
|
||||
import { useHeaderBar } from '@/hooks/core/useHeaderBar'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
import { formatMenuTitle } from '@/utils/router'
|
||||
import ArtUserMenu from '../art-header-bar/widget/ArtUserMenu.vue'
|
||||
|
||||
defineOptions({ name: 'ArtPortalHeader' })
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { homePath } = useCommon()
|
||||
const { shouldShowFullscreen } = useHeaderBar()
|
||||
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
|
||||
const settingStore = useSettingStore()
|
||||
const { menuOpen } = storeToRefs(settingStore)
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
const matched = route.matched
|
||||
if (!matched.length) return ''
|
||||
const current = matched[matched.length - 1]
|
||||
const title = current.meta?.title
|
||||
return title ? formatMenuTitle(String(title)) : ''
|
||||
})
|
||||
|
||||
const toHome = (): void => {
|
||||
router.push(homePath.value)
|
||||
@@ -50,14 +73,20 @@
|
||||
const toggleFullScreen = (): void => {
|
||||
toggleFullscreen()
|
||||
}
|
||||
|
||||
const toggleSidebar = (): void => {
|
||||
settingStore.setMenuOpen(!menuOpen.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.portal-header {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
padding: 0 var(--mint-content-padding, 24px);
|
||||
@@ -66,6 +95,15 @@
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.portal-header-start {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.portal-header-brand {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
@@ -93,15 +131,33 @@
|
||||
|
||||
.portal-header-nav {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.portal-header-sub-nav {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
.portal-header-center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
max-width: min(520px, calc(100% - 420px));
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.portal-header-page-title {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.portal-header-actions {
|
||||
@@ -109,11 +165,11 @@
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:deep(.portal-module-select-trigger) {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.28);
|
||||
background: transparent;
|
||||
|
||||
.portal-module-select-trigger__icon,
|
||||
.portal-module-select-trigger__label,
|
||||
@@ -122,58 +178,7 @@
|
||||
}
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border-color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.portal-sub-nav-card__icon),
|
||||
:deep(.portal-sub-nav-card__label) {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
:deep(.portal-sub-nav-menu > .el-sub-menu .el-sub-menu__icon-arrow) {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
:deep(.portal-sub-nav-menu > .el-sub-menu.el-sub-menu__hide-arrow .el-sub-menu__title) {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.portal-sub-nav-menu > .el-sub-menu.el-sub-menu__hide-arrow .el-sub-menu__icon-more),
|
||||
:deep(.portal-sub-nav-menu > .el-sub-menu.el-sub-menu__hide-arrow .el-icon) {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
:deep(.portal-sub-nav-menu > .el-sub-menu.el-sub-menu__hide-arrow:hover .el-sub-menu__title) {
|
||||
background: rgba(255, 255, 255, 0.14) !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
:deep(.portal-sub-nav-menu > .el-menu-item:hover .portal-sub-nav-card),
|
||||
:deep(.portal-sub-nav-menu > .el-sub-menu:hover .portal-sub-nav-card) {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
|
||||
:deep(.portal-sub-nav-menu > .el-menu-item.is-active .portal-sub-nav-card),
|
||||
:deep(.portal-sub-nav-menu > .el-sub-menu.is-active .portal-sub-nav-card) {
|
||||
background: #fff;
|
||||
|
||||
.portal-sub-nav-card__icon,
|
||||
.portal-sub-nav-card__label {
|
||||
color: var(--mint-primary, #1ca851);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.portal-mobile-menu-trigger) {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.28);
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border-color: rgba(255, 255, 255, 0.45);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,8 +195,12 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.portal-header-sub-nav {
|
||||
display: none !important;
|
||||
.portal-header-center {
|
||||
max-width: min(240px, calc(100% - 220px));
|
||||
}
|
||||
|
||||
.portal-header-page-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -119,12 +119,9 @@
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
cursor: pointer;
|
||||
background: var(--primary-bg, #e6f8ed);
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
@@ -149,12 +146,13 @@
|
||||
}
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
border-color: var(--primary, #1ca851);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
cursor: not-allowed;
|
||||
background: var(--mint-bg-page, #f8fafc);
|
||||
background: transparent;
|
||||
opacity: 0.72;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -173,34 +173,6 @@ export function usePlPlanArrangeActions() {
|
||||
ElMessage.success('已复制司机链接')
|
||||
}
|
||||
|
||||
function buildAllSiteLinksText(records?: TransportSiteLink[]) {
|
||||
const list = records || siteRecords.value
|
||||
return list
|
||||
.filter(item => item.siteUrl)
|
||||
.map(item => `${item.productName || '现场链接'}:${item.siteUrl}`)
|
||||
.join('\n')
|
||||
}
|
||||
|
||||
async function copySiteUrl(url?: string) {
|
||||
const target = (url || siteRecords.value.find(item => item.siteUrl)?.siteUrl || '').trim()
|
||||
if (!target) {
|
||||
ElMessage.warning('暂无现场链接')
|
||||
return
|
||||
}
|
||||
await navigator.clipboard.writeText(target)
|
||||
ElMessage.success('已复制现场链接')
|
||||
}
|
||||
|
||||
async function copyAllSiteUrls(records?: TransportSiteLink[]) {
|
||||
const text = buildAllSiteLinksText(records)
|
||||
if (!text) {
|
||||
ElMessage.warning('暂无现场链接')
|
||||
return
|
||||
}
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success('已复制全部现场链接')
|
||||
}
|
||||
|
||||
function resolveSiteRecordProductText(record: TransportSiteLink) {
|
||||
const product = products.value.find(item => item.id === record.planProductId)
|
||||
if (product) {
|
||||
@@ -316,8 +288,6 @@ export function usePlPlanArrangeActions() {
|
||||
buildPlanSummaryText,
|
||||
buildSiteSummaryText,
|
||||
copyDriverUrl,
|
||||
copySiteUrl,
|
||||
copyAllSiteUrls,
|
||||
copySiteSummary,
|
||||
copyPlanSummary,
|
||||
copyPoundSlipQrcode
|
||||
|
||||
@@ -45,30 +45,20 @@ export function formatPlProductNameWithHazardMarks(product: HazardFlagProduct) {
|
||||
return `${name}${marks.join(' ')}`
|
||||
}
|
||||
|
||||
/** 从 CRM 产品主数据补全易制毒/易制爆标识(合同明细通常不带该字段) */
|
||||
/** 从 CRM 产品主数据读取易制毒/易制爆标识(覆盖计划产品快照字段) */
|
||||
export async function enrichPlanProductsHazardFromCrm<T extends HazardFlagProduct & { productId: string }>(
|
||||
products: T[]
|
||||
): Promise<T[]> {
|
||||
if (!products.length) return products
|
||||
|
||||
const needLookupIds = [
|
||||
...new Set(
|
||||
products
|
||||
.filter(product => {
|
||||
if (!product.productId) return false
|
||||
const flags = resolveProductHazardFlags(product)
|
||||
const needsHazard = !flags.isToxic && !flags.isExplosive
|
||||
const needsTaxRate = product.taxRate == null
|
||||
return needsHazard || needsTaxRate
|
||||
})
|
||||
.map(product => product.productId)
|
||||
)
|
||||
const lookupIds = [
|
||||
...new Set(products.map(product => product.productId).filter(Boolean))
|
||||
]
|
||||
if (!needLookupIds.length) return products
|
||||
if (!lookupIds.length) return products
|
||||
|
||||
const hazardMap = new Map<string, { isToxic: number; isExplosive: number; taxRate?: number }>()
|
||||
await Promise.all(
|
||||
needLookupIds.map(async productId => {
|
||||
lookupIds.map(async productId => {
|
||||
try {
|
||||
const res = await fetchProductDetail({ id: productId })
|
||||
const data = (res as { data?: Record<string, unknown> }).data ?? res
|
||||
@@ -86,13 +76,12 @@ export async function enrichPlanProductsHazardFromCrm<T extends HazardFlagProduc
|
||||
)
|
||||
|
||||
return products.map(product => {
|
||||
const flags = resolveProductHazardFlags(product)
|
||||
const fromCrm = hazardMap.get(product.productId)
|
||||
if (!fromCrm) return product
|
||||
return {
|
||||
...product,
|
||||
isToxic: flags.isToxic ? product.isToxic : fromCrm.isToxic,
|
||||
isExplosive: flags.isExplosive ? product.isExplosive : fromCrm.isExplosive,
|
||||
isToxic: fromCrm.isToxic,
|
||||
isExplosive: fromCrm.isExplosive,
|
||||
taxRate: product.taxRate ?? fromCrm.taxRate
|
||||
}
|
||||
})
|
||||
@@ -104,6 +93,32 @@ export function findMissingHazardCerts(products: PlanProduct[]) {
|
||||
)
|
||||
}
|
||||
|
||||
export function planHasHazardProducts(products: PlanProduct[] | undefined) {
|
||||
return (products ?? []).some(p => isHazardProduct(p))
|
||||
}
|
||||
|
||||
export function isHazardCertReady(plan: { hazardCertReady?: number } | null | undefined) {
|
||||
return Number(plan?.hazardCertReady ?? 0) === 1
|
||||
}
|
||||
|
||||
/** 含危化品且尚未人工确认证书齐备时,阻断安排运输 */
|
||||
export function shouldBlockTransportForHazardCert(
|
||||
plan: { hazardCertReady?: number; products?: PlanProduct[] } | null | undefined,
|
||||
products?: PlanProduct[]
|
||||
) {
|
||||
const prods = products ?? plan?.products
|
||||
if (!planHasHazardProducts(prods)) return false
|
||||
return !isHazardCertReady(plan)
|
||||
}
|
||||
|
||||
export function buildMissingHazardCertReadyMessage(
|
||||
plan: { hazardCertReady?: number; products?: PlanProduct[] } | null | undefined,
|
||||
products?: PlanProduct[]
|
||||
) {
|
||||
if (!shouldBlockTransportForHazardCert(plan, products)) return ''
|
||||
return '该计划危化品证书尚未确认齐备'
|
||||
}
|
||||
|
||||
export function buildMissingHazardCertMessage(products: PlanProduct[]) {
|
||||
const missing = findMissingHazardCerts(products)
|
||||
if (!missing.length) return ''
|
||||
|
||||
@@ -30,7 +30,7 @@ export const SETTING_DEFAULT_CONFIG = {
|
||||
/** 菜单类型 */
|
||||
menuType: MenuTypeEnum.PORTAL,
|
||||
/** 菜单展开宽度 */
|
||||
menuOpenWidth: 230,
|
||||
menuOpenWidth: 220,
|
||||
/** 菜单是否展开 */
|
||||
menuOpen: true,
|
||||
/** 双菜单是否显示文本 */
|
||||
|
||||
@@ -18,6 +18,13 @@ export const plRoutes: AppRouteRecord[] = [
|
||||
component: '/pl/carrier/index',
|
||||
meta: { title: '承运商管理', keepAlive: true, menuCode: 'pl:carrier:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL00000000000000000000000000003',
|
||||
name: 'PlVehicle',
|
||||
path: 'vehicle',
|
||||
component: '/pl/vehicle/index',
|
||||
meta: { title: '全部车辆管理', keepAlive: true, menuCode: 'pl:vehicle:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL00000000000000000000000000003S',
|
||||
name: 'PlSelfVehicle',
|
||||
@@ -26,11 +33,11 @@ export const plRoutes: AppRouteRecord[] = [
|
||||
meta: { title: '山峡自有车辆', keepAlive: true, menuCode: 'pl:self-vehicle:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL00000000000000000000000000003',
|
||||
name: 'PlVehicle',
|
||||
path: 'vehicle',
|
||||
component: '/pl/vehicle/index',
|
||||
meta: { title: '车辆管理', keepAlive: true, menuCode: 'pl:vehicle:list' }
|
||||
id: 'PL00000000000000000000000000004',
|
||||
name: 'PlPersonnel',
|
||||
path: 'personnel',
|
||||
component: '/pl/personnel/index',
|
||||
meta: { title: '驾押人员管理', keepAlive: true, menuCode: 'pl:driver:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL00000000000000000000000000004A',
|
||||
@@ -39,13 +46,6 @@ export const plRoutes: AppRouteRecord[] = [
|
||||
component: '/pl/transport-address/index',
|
||||
meta: { title: '地址库', keepAlive: true, menuCode: 'pl:transport-address:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL00000000000000000000000000004',
|
||||
name: 'PlPersonnel',
|
||||
path: 'personnel',
|
||||
component: '/pl/personnel/index',
|
||||
meta: { title: '人员管理', keepAlive: true, menuCode: 'pl:driver:list' }
|
||||
},
|
||||
{
|
||||
id: 'PL00000000000000000000000000004R',
|
||||
name: 'PlDriverRedirect',
|
||||
|
||||
@@ -458,7 +458,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
path: 'plans',
|
||||
component: '/db/plans/index',
|
||||
meta: {
|
||||
title: '计划管理',
|
||||
title: '地磅计划',
|
||||
keepAlive: true,
|
||||
menuCode: 'db:plan:list'
|
||||
}
|
||||
@@ -469,7 +469,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
path: 'inventory',
|
||||
component: '/db/inventory/index',
|
||||
meta: {
|
||||
title: '出入库管理',
|
||||
title: '地磅出入库',
|
||||
keepAlive: true,
|
||||
menuCode: 'db:inventory:list'
|
||||
}
|
||||
|
||||
@@ -316,23 +316,22 @@
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* 列表合计:第 4 列常为「本页合计(n单)」加粗(备用金申请页除外,见下条) */
|
||||
.table-scroll.table-page-summary:not(.advance-apply-summary-merge)
|
||||
.el-table__footer-wrapper
|
||||
td:nth-child(4)
|
||||
.cell {
|
||||
/* 列表合计:金额列等 footer 单元格样式(如有需要可在此扩展) */
|
||||
.table-scroll.table-page-summary .el-table__footer-wrapper td:nth-child(4) .cell {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* 备用金申请:合计行首格 colspan=4 合并多选+序号+单号+申请人 */
|
||||
.table-scroll.table-page-summary.advance-apply-summary-merge
|
||||
.el-table__footer-wrapper
|
||||
td:first-child
|
||||
.cell {
|
||||
.table-scroll.table-page-summary .el-table__footer-wrapper td.col-apply-code .cell {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 合并申请单列表:金额合计按备用金 / 报销单分列展示 */
|
||||
.table-scroll.table-page-summary-biz-split .el-table__footer-wrapper td.col-money .cell {
|
||||
white-space: normal;
|
||||
line-height: 1.45;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.bx-table .col-actions .cell > * {
|
||||
@@ -463,7 +462,7 @@
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 64px);
|
||||
max-height: var(--dialog-max-height, calc(100vh - 40px));
|
||||
}
|
||||
|
||||
.bx-dialog .el-dialog__header {
|
||||
@@ -483,7 +482,7 @@
|
||||
padding: 20px 22px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
max-height: calc(100vh - 200px);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.bx-dialog .el-dialog__footer {
|
||||
@@ -605,6 +604,27 @@
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.dlg-view-section-tabs {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.dlg-view-flow-tabs {
|
||||
.el-tabs__header {
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
background-color: var(--border);
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* @deprecated 旧版左右 KV,迁移至 BxViewKvGrid 后删除 */
|
||||
.dlg-kv {
|
||||
display: grid;
|
||||
@@ -945,6 +965,10 @@
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.offset-pager + .el-form-item {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.list-toolbar-actions .bx-btn.el-button:not(.el-button--primary) {
|
||||
background: var(--surface);
|
||||
border-color: var(--border);
|
||||
@@ -955,3 +979,31 @@
|
||||
background: var(--row-alt);
|
||||
}
|
||||
|
||||
/* 报销新增/编辑弹窗:label 左、控件右,垂直居中,label 可换行(栅格列数不变) */
|
||||
.bx-dialog-form {
|
||||
.el-form-item {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
[class*='form-grid-'] > .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
[class*='form-grid-'] {
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
height: auto !important;
|
||||
padding-right: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
> .list-toolbar-selected,
|
||||
> .list-toolbar-date-shortcuts,
|
||||
> .el-input,
|
||||
> .quick-search-group,
|
||||
> .quick-search-input,
|
||||
> .el-button {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -17,7 +18,7 @@
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.db-toolbar-quick-search-input.el-input {
|
||||
.quick-search-group .quick-search-input {
|
||||
width: 360px;
|
||||
max-width: 360px;
|
||||
flex: 0 0 360px;
|
||||
@@ -53,10 +54,6 @@
|
||||
--el-button-hover-border-color: rgba(28, 168, 81, 0.45);
|
||||
}
|
||||
|
||||
.db-quick-search-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.db-op-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -502,3 +499,33 @@
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 地磅新增/编辑弹窗:label 左、控件右,垂直居中,label 可换行(栅格列数不变) */
|
||||
.db-dialog-form {
|
||||
.el-form-item {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&[class*='form-grid-'] > .el-form-item,
|
||||
[class*='form-grid-'] > .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&[class*='form-grid-'],
|
||||
[class*='form-grid-'] {
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
height: auto !important;
|
||||
padding-right: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* 运输(pl)模块样式 */
|
||||
|
||||
/* 新增/编辑弹窗:label 左、控件右,垂直居中,label 可换行(栅格列数不变) */
|
||||
.pl-dialog-form {
|
||||
.el-form-item {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&[class*='form-grid-'] > .el-form-item,
|
||||
[class*='form-grid-'] > .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&[class*='form-grid-'],
|
||||
[class*='form-grid-'] {
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
height: auto !important;
|
||||
padding-right: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
--van-success-color: #1ca851;
|
||||
--van-button-primary-background: #1ca851;
|
||||
--van-button-primary-border-color: #1ca851;
|
||||
--van-nav-bar-background: #e6f8ed;
|
||||
--van-nav-bar-title-text-color: #168f45;
|
||||
--van-nav-bar-background: #059669;
|
||||
--van-nav-bar-title-text-color: #fff;
|
||||
--van-cell-group-inset-padding: 0;
|
||||
--van-uploader-size: 96px;
|
||||
--van-uploader-upload-background: #e6f8ed;
|
||||
@@ -13,16 +13,70 @@
|
||||
}
|
||||
|
||||
.public-pl-transport-page {
|
||||
--van-cell-group-title-padding: 2px 10px;
|
||||
--van-cell-group-inset-title-padding: 2px 10px;
|
||||
--van-cell-group-title-font-size: 12px;
|
||||
--van-cell-group-title-line-height: 1.1;
|
||||
--van-button-primary-background: #059669;
|
||||
--van-button-primary-border-color: #059669;
|
||||
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .section-card .van-cell-group__title,
|
||||
.public-pl-transport-page .section-card .van-cell-group__title--inset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 10px;
|
||||
margin: 0;
|
||||
min-height: 0;
|
||||
line-height: 1.1;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #047857;
|
||||
letter-spacing: 0.01em;
|
||||
background: linear-gradient(90deg, #ecfdf5 0%, rgb(236 253 245 / 0%) 72%);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
flex-shrink: 0;
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
background: #059669;
|
||||
}
|
||||
}
|
||||
|
||||
.public-pl-transport-page .section-card .van-cell {
|
||||
padding: 7px 14px;
|
||||
line-height: 1.35;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .section-card .van-field {
|
||||
padding: 4px 14px 10px;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .section-card .van-field__control {
|
||||
line-height: 1.35;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .section-card .van-cell::after {
|
||||
right: 14px;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .van-cell__title {
|
||||
color: #64748b;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .van-cell__value {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .route-cell .van-cell__value {
|
||||
@@ -30,18 +84,26 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .product-cell .van-cell__title {
|
||||
.public-pl-transport-page .product-cell .van-cell__title,
|
||||
.public-pl-site-page .product-cell .van-cell__title {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
line-height: 1.35;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .product-cell .van-cell__label {
|
||||
.public-pl-transport-page .product-cell .van-cell__label,
|
||||
.public-pl-site-page .product-cell .van-cell__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .page-footer {
|
||||
.public-pl-transport-page .product-cell .van-cell__value,
|
||||
.public-pl-site-page .product-cell .van-cell__value {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.public-pl-transport-page .page-footer,
|
||||
.public-pl-site-page .page-footer {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
@@ -51,4 +113,76 @@
|
||||
background: rgba(248, 250, 252, 0.96);
|
||||
border-top: 1px solid #d9e8e2;
|
||||
box-sizing: border-box;
|
||||
|
||||
.van-button--primary {
|
||||
background: #059669;
|
||||
border-color: #059669;
|
||||
}
|
||||
}
|
||||
|
||||
.public-pl-site-page {
|
||||
--van-cell-group-title-padding: 2px 10px;
|
||||
--van-cell-group-inset-title-padding: 2px 10px;
|
||||
--van-cell-group-title-font-size: 12px;
|
||||
--van-cell-group-title-line-height: 1.1;
|
||||
--van-button-primary-background: #059669;
|
||||
--van-button-primary-border-color: #059669;
|
||||
|
||||
padding-bottom: 72px;
|
||||
}
|
||||
|
||||
.public-pl-site-page .section-card .van-cell-group__title,
|
||||
.public-pl-site-page .section-card .van-cell-group__title--inset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 2px 10px;
|
||||
margin: 0;
|
||||
min-height: 0;
|
||||
line-height: 1.1;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #047857;
|
||||
letter-spacing: 0.01em;
|
||||
background: linear-gradient(90deg, #ecfdf5 0%, rgb(236 253 245 / 0%) 72%);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
flex-shrink: 0;
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
background: #059669;
|
||||
}
|
||||
}
|
||||
|
||||
.public-pl-site-page .section-card .van-cell {
|
||||
padding: 7px 14px;
|
||||
line-height: 1.35;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.public-pl-site-page .section-card .van-field {
|
||||
padding: 4px 14px 10px;
|
||||
}
|
||||
|
||||
.public-pl-site-page .section-card .van-field__control {
|
||||
line-height: 1.35;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.public-pl-site-page .section-card .van-cell::after {
|
||||
right: 14px;
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.public-pl-site-page .van-cell__title {
|
||||
color: #64748b;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.public-pl-site-page .van-cell__value {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* 系统管理(sys)模块样式 */
|
||||
|
||||
/* 新增/编辑弹窗:label 左、控件右,垂直居中,label 可换行(栅格列数不变) */
|
||||
.sys-dialog-form {
|
||||
.el-form-item {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&[class*='form-grid-'] > .el-form-item,
|
||||
[class*='form-grid-'] > .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&[class*='form-grid-'],
|
||||
[class*='form-grid-'] {
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
height: auto !important;
|
||||
padding-right: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/* 工作流(wf)模块样式 */
|
||||
|
||||
/* 新增/编辑弹窗:label 左、控件右,垂直居中,label 可换行(栅格列数不变) */
|
||||
.wf-dialog-form {
|
||||
.el-form-item {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&[class*='form-grid-'] > .el-form-item,
|
||||
[class*='form-grid-'] > .el-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&[class*='form-grid-'],
|
||||
[class*='form-grid-'] {
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
line-height: 1.45;
|
||||
height: auto !important;
|
||||
padding-right: 12px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.el-form-item__content {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,41 @@ export function buildAmapNavigationUrl(
|
||||
return `https://uri.amap.com/navigation?to=${lngText},${latText},${toName}&mode=car&coordinate=gaode&callnative=1`
|
||||
}
|
||||
|
||||
/** 构建高德 URI 标记点链接(Web/App 查看位置) */
|
||||
export function buildAmapMarkerUrl(
|
||||
lng?: number | null,
|
||||
lat?: number | null,
|
||||
name?: string
|
||||
): string | null {
|
||||
if (lng == null || lat == null || Number.isNaN(Number(lng)) || Number.isNaN(Number(lat))) {
|
||||
return null
|
||||
}
|
||||
const lngText = Number(lng).toFixed(6)
|
||||
const latText = Number(lat).toFixed(6)
|
||||
const markerName = encodeURIComponent((name || '位置').trim() || '位置')
|
||||
return `https://uri.amap.com/marker?position=${lngText},${latText}&name=${markerName}&coordinate=gaode&callnative=0`
|
||||
}
|
||||
|
||||
/** 按地址关键词打开高德地图搜索(无坐标时的兜底) */
|
||||
export function buildAmapSearchUrl(keyword?: string | null): string | null {
|
||||
const text = keyword?.trim()
|
||||
if (!text) return null
|
||||
return `https://www.amap.com/search?query=${encodeURIComponent(text)}`
|
||||
}
|
||||
|
||||
/** 优先坐标标记,否则按地址搜索 */
|
||||
export function buildAmapLocationUrl(options: {
|
||||
lng?: number | null
|
||||
lat?: number | null
|
||||
name?: string
|
||||
address?: string
|
||||
}): string | null {
|
||||
return (
|
||||
buildAmapMarkerUrl(options.lng, options.lat, options.name || options.address) ||
|
||||
buildAmapSearchUrl(options.address || options.name)
|
||||
)
|
||||
}
|
||||
|
||||
export function appendAmapNavLine(lines: string[], label: string, url: string | null) {
|
||||
if (url) {
|
||||
lines.push(`${label}:${url}`)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { formatSumMoney } from './format'
|
||||
|
||||
export interface SummaryColumn {
|
||||
property?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
export interface PageSummaryParams {
|
||||
columns: SummaryColumn[]
|
||||
data: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
export interface BuildPageSummaryOptions {
|
||||
/** 放置合计标签的列 prop,默认 applyCode;默认不展示标签文案 */
|
||||
labelProp?: string
|
||||
/** 自定义标签文案;默认返回空字符串 */
|
||||
labelFormatter?: (count: number) => string
|
||||
/** 列 prop -> 合计值(已格式化字符串) */
|
||||
sumByProp?: Record<string, (data: Record<string, unknown>[]) => string>
|
||||
}
|
||||
|
||||
/** 对数值字段求和并格式化为合计金额 */
|
||||
export function sumField(
|
||||
data: Record<string, unknown>[],
|
||||
field: string,
|
||||
optional = false
|
||||
): string {
|
||||
const total = data.reduce((sum, row) => {
|
||||
const value = row[field]
|
||||
if (optional && (value == null || value === '')) return sum
|
||||
return sum + Number(value || 0)
|
||||
}, 0)
|
||||
return formatSumMoney(total)
|
||||
}
|
||||
|
||||
/** 按业务类型(备用金 / 报销单)分列求和,用于合并列表合计行 */
|
||||
export function sumFieldByBizType(
|
||||
data: Record<string, unknown>[],
|
||||
field: string,
|
||||
bizTypeField = 'bizType'
|
||||
): string {
|
||||
let advanceTotal = 0
|
||||
let reimburseTotal = 0
|
||||
for (const row of data) {
|
||||
const amount = Number(row[field] || 0)
|
||||
const bizType = row[bizTypeField]
|
||||
if (bizType === '备用金') advanceTotal += amount
|
||||
else if (bizType === '报销单') reimburseTotal += amount
|
||||
}
|
||||
return `备用金总额 ${formatSumMoney(advanceTotal)} / 报销单总额 ${formatSumMoney(reimburseTotal)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Element Plus 表格合计行方法;按当前可见 columns 动态对齐,兼容列显隐。
|
||||
*/
|
||||
export function buildPageSummaryMethod(options: BuildPageSummaryOptions = {}) {
|
||||
const {
|
||||
labelProp = 'applyCode',
|
||||
labelFormatter = () => '',
|
||||
sumByProp = {}
|
||||
} = options
|
||||
|
||||
return ({ columns, data }: PageSummaryParams) => {
|
||||
const label = labelFormatter(data.length)
|
||||
return columns.map((col) => {
|
||||
const prop = col.property
|
||||
if (prop === labelProp) return label
|
||||
if (prop && sumByProp[prop]) return sumByProp[prop](data)
|
||||
return ''
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { TagType, ViewField } from '@/components/business/system-view-dialog/types'
|
||||
import type { TagType, ViewField, ViewHighlight } from '@/components/business/system-view-dialog/types'
|
||||
import { formatViewValue } from '@/components/business/system-view-dialog/types'
|
||||
|
||||
type DictStoreLike = {
|
||||
@@ -37,3 +37,11 @@ export function crmTagField(
|
||||
): ViewField {
|
||||
return { label, value: display || '—', tagType, ...extra }
|
||||
}
|
||||
|
||||
export function crmHighlight(
|
||||
label: string,
|
||||
value: unknown,
|
||||
extra?: Partial<ViewHighlight>
|
||||
): ViewHighlight {
|
||||
return { label, value: formatViewValue(value), ...extra }
|
||||
}
|
||||
@@ -112,6 +112,29 @@ export function formatPlanProductListText(
|
||||
return `${name}${specPart},${qtyUnit}`
|
||||
}
|
||||
|
||||
/** 司机公开页产品一行展示(含运输计划产品备注) */
|
||||
export function formatPublicTransportProductLine(product: {
|
||||
productName?: string
|
||||
model?: string
|
||||
packageSpec?: string
|
||||
unit?: string
|
||||
quantity?: number | string | null
|
||||
remark?: string
|
||||
}) {
|
||||
const name = product.productName?.trim() || '—'
|
||||
const parts: string[] = []
|
||||
const model = product.model?.trim()
|
||||
const packageSpec = product.packageSpec?.trim()
|
||||
if (model) parts.push(model)
|
||||
if (packageSpec) parts.push(packageSpec)
|
||||
const qty = getPlanProductPlannedQuantity(product)
|
||||
const unitRaw = product.unit || '吨'
|
||||
parts.push(qty !== '—' ? `${qty} ${unitRaw}` : unitRaw)
|
||||
const remark = product.remark?.trim()
|
||||
parts.push(`备注:${remark || '—'}`)
|
||||
return `${name}(${parts.join(',')})`
|
||||
}
|
||||
|
||||
export function getPlPlanCompanyLabel(company?: string) {
|
||||
const map: Record<string, string> = { shanxia: '安庆山峡', yundong: '安徽云栋' }
|
||||
return company ? map[company] || company : '—'
|
||||
@@ -142,7 +165,7 @@ export function getPlPlanFollowStatusLabel(status?: string) {
|
||||
draft: '草稿',
|
||||
processing: '进行中',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消'
|
||||
cancelled: '已作废'
|
||||
}
|
||||
return status ? map[status] || status : '—'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { CertRow } from '@/api/pl'
|
||||
|
||||
export const PL_PERSONNEL_CERT_MISSING_SUFFIX = '(证书缺失)'
|
||||
|
||||
export const PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES = [
|
||||
'身份证',
|
||||
'驾驶证',
|
||||
'从业资格证'
|
||||
] as const
|
||||
|
||||
export const PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES = ['身份证', '从业资格证'] as const
|
||||
|
||||
export type PersonnelCertRole = 'driver' | 'escort'
|
||||
|
||||
export function matchRequiredCertName(
|
||||
actualName: string | undefined,
|
||||
requiredName: string
|
||||
): boolean {
|
||||
const name = actualName?.trim() || ''
|
||||
if (!name) return false
|
||||
if (requiredName === '从业资格证') {
|
||||
return (
|
||||
name === '从业资格证' ||
|
||||
name === '危化品从业资格证' ||
|
||||
name.includes('从业资格证')
|
||||
)
|
||||
}
|
||||
return name === requiredName
|
||||
}
|
||||
|
||||
export function isPersonnelCertUploaded(cert?: Pick<CertRow, 'certName' | 'attachmentUrls'>) {
|
||||
return Boolean(cert?.certName?.trim() && cert?.attachmentUrls?.trim())
|
||||
}
|
||||
|
||||
export function getRequiredPersonnelCertNames(role: PersonnelCertRole): readonly string[] {
|
||||
return role === 'driver'
|
||||
? PL_PERSONNEL_DRIVER_REQUIRED_CERT_NAMES
|
||||
: PL_PERSONNEL_ESCORT_REQUIRED_CERT_NAMES
|
||||
}
|
||||
|
||||
export function getMissingPersonnelCertNames(
|
||||
role: PersonnelCertRole,
|
||||
certList: CertRow[] = []
|
||||
): string[] {
|
||||
return getRequiredPersonnelCertNames(role).filter(requiredName => {
|
||||
const matched = certList.find(cert => matchRequiredCertName(cert.certName, requiredName))
|
||||
return !isPersonnelCertUploaded(matched)
|
||||
})
|
||||
}
|
||||
|
||||
export function hasPersonnelCertMissing(
|
||||
role: PersonnelCertRole,
|
||||
certList: CertRow[] = []
|
||||
): boolean {
|
||||
return getMissingPersonnelCertNames(role, certList).length > 0
|
||||
}
|
||||
|
||||
/** 人员主数据页:至少满足押运员必备证书(身份证 + 从业资格证) */
|
||||
export function hasPersonnelMasterCertMissing(certList: CertRow[] = []): boolean {
|
||||
return hasPersonnelCertMissing('escort', certList)
|
||||
}
|
||||
|
||||
export function getPersonnelCertMissingSuffix(
|
||||
certList: CertRow[] = [],
|
||||
role?: PersonnelCertRole
|
||||
): string {
|
||||
if (role) {
|
||||
return hasPersonnelCertMissing(role, certList) ? PL_PERSONNEL_CERT_MISSING_SUFFIX : ''
|
||||
}
|
||||
return hasPersonnelMasterCertMissing(certList) ? PL_PERSONNEL_CERT_MISSING_SUFFIX : ''
|
||||
}
|
||||
@@ -3,8 +3,24 @@ import type { CertRow } from '@/api/pl'
|
||||
export const PL_VEHICLE_CERT_PART_HEAD = 'head'
|
||||
export const PL_VEHICLE_CERT_PART_TRAILER = 'trailer'
|
||||
|
||||
export const PL_VEHICLE_CERT_MISSING_SUFFIX = '(证书缺失)'
|
||||
|
||||
/** @deprecated 保存不再阻断,仅保留供历史引用 */
|
||||
export const PL_VEHICLE_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
|
||||
|
||||
export const PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES = [
|
||||
'行驶证',
|
||||
'营运证',
|
||||
'保险单'
|
||||
] as const
|
||||
|
||||
export const PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES = [
|
||||
'行驶证',
|
||||
'营运证',
|
||||
'保险单',
|
||||
'罐检报告'
|
||||
] as const
|
||||
|
||||
export const PL_VEHICLE_CERT_NAME_OPTIONS = [
|
||||
'行驶证',
|
||||
'营运证',
|
||||
@@ -12,7 +28,8 @@ export const PL_VEHICLE_CERT_NAME_OPTIONS = [
|
||||
'驾驶证',
|
||||
'押运员证',
|
||||
'危化品从业资格证',
|
||||
'保险单'
|
||||
'保险单',
|
||||
'罐检报告'
|
||||
]
|
||||
|
||||
export type VehicleCertPart = typeof PL_VEHICLE_CERT_PART_HEAD | typeof PL_VEHICLE_CERT_PART_TRAILER
|
||||
@@ -53,16 +70,61 @@ export function mergeVehicleCertsByPart<T extends CertRow>(
|
||||
return [...normalizedHead, ...normalizedTrailer]
|
||||
}
|
||||
|
||||
function isCertFilled(cert?: CertRow) {
|
||||
if (!cert) return false
|
||||
return Boolean(
|
||||
cert.certName?.trim() &&
|
||||
cert.certNo?.trim() &&
|
||||
cert.expireDate &&
|
||||
cert.attachmentUrls?.trim()
|
||||
)
|
||||
export function isVehicleCertUploaded(cert?: Pick<CertRow, 'certName' | 'attachmentUrls'>) {
|
||||
return Boolean(cert?.certName?.trim() && cert?.attachmentUrls?.trim())
|
||||
}
|
||||
|
||||
export function getMissingSelfVehiclePartCertNames(
|
||||
certList: CertRow[] = [],
|
||||
part: VehicleCertPart
|
||||
): string[] {
|
||||
const requiredNames =
|
||||
part === PL_VEHICLE_CERT_PART_TRAILER
|
||||
? PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
: PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES
|
||||
|
||||
return requiredNames.filter(requiredName => {
|
||||
const matched = certList.find(cert => cert.certName?.trim() === requiredName)
|
||||
return !isVehicleCertUploaded(matched)
|
||||
})
|
||||
}
|
||||
|
||||
export function hasSelfVehicleCertMissing(options: {
|
||||
certList?: CertRow[]
|
||||
trailerNo?: string | null
|
||||
}): boolean {
|
||||
const { certList = [], trailerNo } = options
|
||||
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList)
|
||||
const hasTrailer = Boolean(trailerNo?.trim())
|
||||
|
||||
if (getMissingSelfVehiclePartCertNames(headCerts, PL_VEHICLE_CERT_PART_HEAD).length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (hasTrailer && getMissingSelfVehiclePartCertNames(trailerCerts, PL_VEHICLE_CERT_PART_TRAILER).length > 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function getSelfVehicleCertMissingSuffix(options: {
|
||||
certList?: CertRow[]
|
||||
trailerNo?: string | null
|
||||
}): string {
|
||||
return hasSelfVehicleCertMissing(options) ? PL_VEHICLE_CERT_MISSING_SUFFIX : ''
|
||||
}
|
||||
|
||||
export function getSelfVehiclePartCertMissingSuffix(
|
||||
certList: CertRow[] = [],
|
||||
part: VehicleCertPart
|
||||
): string {
|
||||
return getMissingSelfVehiclePartCertNames(certList, part).length > 0
|
||||
? PL_VEHICLE_CERT_MISSING_SUFFIX
|
||||
: ''
|
||||
}
|
||||
|
||||
/** @deprecated 保存不再阻断证书完整性,仅校验证书名称非空等基础项 */
|
||||
export function validateVehicleRequiredCerts(options: {
|
||||
headCerts: CertRow[]
|
||||
trailerCerts: CertRow[]
|
||||
@@ -71,28 +133,12 @@ export function validateVehicleRequiredCerts(options: {
|
||||
trailerNo: string
|
||||
onError: (message: string) => void
|
||||
}) {
|
||||
const { headCerts, trailerCerts, hasTrailer, licensePlate, trailerNo, onError } = options
|
||||
const parts: Array<{ label: string; certs: CertRow[] }> = [
|
||||
{ label: licensePlate?.trim() || '主车', certs: headCerts }
|
||||
]
|
||||
if (hasTrailer) {
|
||||
parts.push({ label: trailerNo?.trim() || '挂车', certs: trailerCerts })
|
||||
}
|
||||
|
||||
for (const part of parts) {
|
||||
for (const certName of PL_VEHICLE_REQUIRED_CERT_NAMES) {
|
||||
const matched = part.certs.find(item => item.certName?.trim() === certName)
|
||||
if (!isCertFilled(matched)) {
|
||||
onError(`请完善「${part.label}」的${certName}(证号、有效期、附件均必填)`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
const { headCerts, trailerCerts, hasTrailer } = options
|
||||
|
||||
for (const certs of [headCerts, ...(hasTrailer ? [trailerCerts] : [])]) {
|
||||
for (const cert of certs) {
|
||||
if (!cert.certName?.trim()) {
|
||||
onError('证书名称不能为空')
|
||||
options.onError('证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -108,3 +154,18 @@ export function filterVehicleCertsForSubmit<T extends CertRow>(
|
||||
) {
|
||||
return mergeVehicleCertsByPart(headCerts, trailerCerts, hasTrailer)
|
||||
}
|
||||
|
||||
export function validateVehicleCertNamesNotEmpty(
|
||||
certLists: CertRow[][],
|
||||
onError: (message: string) => void
|
||||
): boolean {
|
||||
for (const certs of certLists) {
|
||||
for (const cert of certs) {
|
||||
if (!cert.certName?.trim()) {
|
||||
onError('证书名称不能为空')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
const TOOLBAR_SELECTOR = '.page-container .list-toolbar, .console-container .list-toolbar'
|
||||
const FILTER_AUTO_TOGGLE_CLASS = 'btn-filter-toggle-auto'
|
||||
const FILTER_TOGGLE_ICON_CLASS = 'btn-list-filter-toggle'
|
||||
const QUICK_SEARCH_GROUP_CLASS = 'quick-search-group'
|
||||
|
||||
const ensureQuickSearchGroup = (quickSearchInput: HTMLElement): HTMLElement => {
|
||||
const parent = quickSearchInput.parentElement
|
||||
if (parent?.classList.contains(QUICK_SEARCH_GROUP_CLASS)) {
|
||||
return parent
|
||||
}
|
||||
|
||||
const group = document.createElement('div')
|
||||
group.className = QUICK_SEARCH_GROUP_CLASS
|
||||
quickSearchInput.replaceWith(group)
|
||||
group.appendChild(quickSearchInput)
|
||||
return group
|
||||
}
|
||||
|
||||
const findPrecedingFilterBlock = (toolbar: HTMLElement): HTMLElement | null => {
|
||||
if (toolbar.closest('.el-dialog')) return null
|
||||
|
||||
let prev = toolbar.previousElementSibling as HTMLElement | null
|
||||
while (prev) {
|
||||
if (prev.classList.contains('filter-block')) return prev
|
||||
if (
|
||||
prev.classList.contains('list-body') ||
|
||||
prev.classList.contains('list-toolbar') ||
|
||||
prev.classList.contains('panel-top') ||
|
||||
prev.classList.contains('stat-grid')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
prev = prev.previousElementSibling as HTMLElement | null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const setFilterBlockExpanded = (
|
||||
filterBlock: HTMLElement,
|
||||
button: HTMLButtonElement,
|
||||
expanded: boolean
|
||||
) => {
|
||||
filterBlock.classList.toggle('is-filter-collapsed', !expanded)
|
||||
filterBlock.classList.toggle('is-filter-expanded', expanded)
|
||||
button.setAttribute('aria-expanded', expanded ? 'true' : 'false')
|
||||
button.setAttribute('aria-label', expanded ? '收起检索' : '展开检索')
|
||||
button.setAttribute('title', expanded ? '收起检索' : '展开检索')
|
||||
}
|
||||
|
||||
const createToggleButton = (filterBlock: HTMLElement): HTMLButtonElement => {
|
||||
const button = document.createElement('button')
|
||||
button.type = 'button'
|
||||
button.className = FILTER_TOGGLE_ICON_CLASS
|
||||
button.setAttribute('aria-expanded', 'false')
|
||||
button.setAttribute('aria-label', '展开检索')
|
||||
button.setAttribute('title', '展开检索')
|
||||
button.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const expanded = filterBlock.classList.contains('is-filter-expanded')
|
||||
setFilterBlockExpanded(filterBlock, button, !expanded)
|
||||
})
|
||||
return button
|
||||
}
|
||||
|
||||
const resolveFilterToggleAnchor = (
|
||||
toolbar: HTMLElement
|
||||
): { anchor: HTMLElement; quickSearchInput: HTMLElement | null } | null => {
|
||||
const quickSearchInput = toolbar.querySelector<HTMLElement>('.quick-search-input')
|
||||
if (quickSearchInput) {
|
||||
return { anchor: ensureQuickSearchGroup(quickSearchInput), quickSearchInput }
|
||||
}
|
||||
|
||||
const selectedAnchor = toolbar.querySelector<HTMLElement>('.list-toolbar-selected')
|
||||
if (selectedAnchor) {
|
||||
return { anchor: selectedAnchor, quickSearchInput: null }
|
||||
}
|
||||
|
||||
const rowAnchor = toolbar.querySelector<HTMLElement>('.list-toolbar-row')
|
||||
if (rowAnchor) {
|
||||
return { anchor: rowAnchor, quickSearchInput: null }
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const ensureToolbarFilterToggle = (toolbar: HTMLElement, filterBlock: HTMLElement) => {
|
||||
if (!filterBlock.classList.contains('is-filter-expanded')) {
|
||||
filterBlock.classList.add('is-filter-collapsed')
|
||||
}
|
||||
|
||||
const resolved = resolveFilterToggleAnchor(toolbar)
|
||||
if (!resolved) return
|
||||
|
||||
const { anchor } = resolved
|
||||
const existingToggle = toolbar.querySelector<HTMLButtonElement>(`.${FILTER_TOGGLE_ICON_CLASS}`)
|
||||
|
||||
if (existingToggle && existingToggle.parentElement !== anchor) {
|
||||
existingToggle.remove()
|
||||
}
|
||||
|
||||
let toggle = anchor.querySelector<HTMLButtonElement>(`:scope > .${FILTER_TOGGLE_ICON_CLASS}`)
|
||||
if (!toggle) {
|
||||
toggle = createToggleButton(filterBlock)
|
||||
anchor.insertBefore(toggle, anchor.firstChild)
|
||||
}
|
||||
|
||||
setFilterBlockExpanded(
|
||||
filterBlock,
|
||||
toggle,
|
||||
filterBlock.classList.contains('is-filter-expanded')
|
||||
)
|
||||
}
|
||||
|
||||
const removeAutoFilterToggles = (): void => {
|
||||
document.querySelectorAll<HTMLElement>(`.${FILTER_AUTO_TOGGLE_CLASS}`).forEach((node) => {
|
||||
node.remove()
|
||||
})
|
||||
}
|
||||
|
||||
export const refreshFilterCollapse = (): void => {
|
||||
removeAutoFilterToggles()
|
||||
|
||||
document.querySelectorAll<HTMLElement>(TOOLBAR_SELECTOR).forEach((toolbar) => {
|
||||
const filterBlock = findPrecedingFilterBlock(toolbar)
|
||||
if (!filterBlock) return
|
||||
ensureToolbarFilterToggle(toolbar, filterBlock)
|
||||
})
|
||||
}
|
||||
|
||||
export const setupFilterCollapseObserver = (): MutationObserver => {
|
||||
refreshFilterCollapse()
|
||||
const observer = new MutationObserver(() => {
|
||||
refreshFilterCollapse()
|
||||
})
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
return observer
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
const INTERACTIVE_SELECTOR =
|
||||
'button, a, .el-link, .el-dropdown, input, textarea, select, .el-switch, .el-radio, .el-select, .btn-op-item, .el-table__expand-icon'
|
||||
|
||||
/**
|
||||
* 主列表行单击切换多选:点击行即选中/取消,点击链接、按钮、展开图标等交互元素时不触发。
|
||||
*/
|
||||
export function setupListTableRowSelect() {
|
||||
const handler = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement | null
|
||||
if (!target) return
|
||||
|
||||
if (target.closest('.el-table-column--selection')) return
|
||||
|
||||
const row = target.closest('.list-table .el-table__body-wrapper tbody tr.el-table__row')
|
||||
if (!row) return
|
||||
|
||||
if (target.closest(INTERACTIVE_SELECTOR)) return
|
||||
|
||||
const checkboxInput = row.querySelector(
|
||||
'.el-table-column--selection .el-checkbox__input'
|
||||
) as HTMLElement | null
|
||||
if (!checkboxInput) return
|
||||
|
||||
checkboxInput.click()
|
||||
}
|
||||
|
||||
document.body.addEventListener('click', handler)
|
||||
return () => document.body.removeEventListener('click', handler)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export function syncViewRowFromFlatList<T extends { id: string | number }>(
|
||||
viewRow: Ref<T | null>,
|
||||
list: T[]
|
||||
) {
|
||||
const current = viewRow.value
|
||||
if (!current?.id) return
|
||||
const updated = list.find(row => row.id === current.id)
|
||||
if (updated) viewRow.value = updated
|
||||
}
|
||||
|
||||
export function findNodeInTreeById<T extends { id: string | number; children?: T[] }>(
|
||||
nodes: T[],
|
||||
id: string | number
|
||||
): T | undefined {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node
|
||||
if (node.children?.length) {
|
||||
const found = findNodeInTreeById(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function syncViewRowFromTree<T extends { id: string | number; children?: T[] }>(
|
||||
viewRow: Ref<T | null>,
|
||||
roots: T[]
|
||||
) {
|
||||
const current = viewRow.value
|
||||
if (!current?.id) return
|
||||
const updated = findNodeInTreeById(roots, current.id)
|
||||
if (updated) viewRow.value = updated
|
||||
}
|
||||
@@ -34,6 +34,7 @@ export function isViewFieldLong(field: ViewField, displayText: string): boolean
|
||||
|
||||
/** 是否占满整行(备注超长、显式 fullSpan / span:'full') */
|
||||
export function isViewFieldFullSpan(field: ViewField, displayText: string): boolean {
|
||||
if (field.fullSpan === false) return false
|
||||
if (field.fullSpan === true || field.span === 'full') return true
|
||||
if (displayText === '—') return false
|
||||
return displayText.length >= VIEW_KV_FULL_MIN_LEN
|
||||
@@ -156,3 +157,36 @@ export function buildViewKvTableQuadRows(
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
export type ViewKvTableQuintRow =
|
||||
| { type: 'long'; field: ViewField }
|
||||
| { type: 'quint'; fields: ViewField[] }
|
||||
|
||||
/** KV 表格模式(五组十列):长字段独占行,短字段最多五个一组 */
|
||||
export function buildViewKvTableQuintRows(
|
||||
fields: ViewField[],
|
||||
formatValue: (val: unknown) => string
|
||||
): ViewKvTableQuintRow[] {
|
||||
const rows: ViewKvTableQuintRow[] = []
|
||||
let i = 0
|
||||
while (i < fields.length) {
|
||||
const field = fields[i]
|
||||
const text = getViewFieldDisplayText(field, formatValue)
|
||||
if (isViewFieldLong(field, text) || isViewFieldFullSpan(field, text)) {
|
||||
rows.push({ type: 'long', field })
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
const group: ViewField[] = [field]
|
||||
i += 1
|
||||
while (i < fields.length && group.length < 5) {
|
||||
const next = fields[i]
|
||||
const nextText = getViewFieldDisplayText(next, formatValue)
|
||||
if (isViewFieldLong(next, nextText) || isViewFieldFullSpan(next, nextText)) break
|
||||
group.push(next)
|
||||
i += 1
|
||||
}
|
||||
rows.push({ type: 'quint', fields: group })
|
||||
}
|
||||
return rows
|
||||
}
|
||||
@@ -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>
|
||||
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