提交
This commit is contained in:
1 parent
d84f4b944c
commit
7c838cfb2d
249 files changed
+50600
-18556
No files matched your search
@@ -22,7 +22,7 @@ function unwrapData(res: any) {
|
||||
function mapListQuery(query: ClueQuery = {}): Record<string, any> {
|
||||
const body: Record<string, any> = {
|
||||
page: query.pageNum ?? 1,
|
||||
pageSize: query.pageSize ?? 10
|
||||
pageSize: query.pageSize ?? 20
|
||||
}
|
||||
if (query.clueName) body.clueName = query.clueName
|
||||
if (query.company) body.company = query.company
|
||||
@@ -197,7 +197,7 @@ export async function listClueFollow(query: ClueFollowQuery) {
|
||||
data: {
|
||||
clueId: String(query.clueId),
|
||||
page: query.pageNum ?? 1,
|
||||
pageSize: query.pageSize ?? 10
|
||||
pageSize: query.pageSize ?? 20
|
||||
}
|
||||
})
|
||||
const data = unwrapData(res)
|
||||
|
||||
@@ -13,7 +13,7 @@ function unwrapData(res: any) {
|
||||
function mapListQuery(query: ApprovalQuery = {}): Record<string, any> {
|
||||
const params: Record<string, any> = {
|
||||
pageNum: query.pageNum ?? 1,
|
||||
pageSize: query.pageSize ?? 10
|
||||
pageSize: query.pageSize ?? 20
|
||||
}
|
||||
;[
|
||||
'contractNo',
|
||||
|
||||
@@ -13,7 +13,7 @@ function unwrapData(res: any) {
|
||||
function mapListQuery(query: ChangeQuery = {}): Record<string, any> {
|
||||
const params: Record<string, any> = {
|
||||
pageNum: query.pageNum ?? 1,
|
||||
pageSize: query.pageSize ?? 10
|
||||
pageSize: query.pageSize ?? 20
|
||||
}
|
||||
;['contractNo', 'contractName', 'changeType', 'approvalStatus', 'beginTime', 'endTime'].forEach((key) => {
|
||||
const val = (query as any)[key]
|
||||
|
||||
@@ -13,7 +13,7 @@ function unwrapData(res: any) {
|
||||
function mapListQuery(query: ContractQuery = {}): Record<string, any> {
|
||||
const params: Record<string, any> = {
|
||||
pageNum: query.pageNum ?? 1,
|
||||
pageSize: query.pageSize ?? 10
|
||||
pageSize: query.pageSize ?? 20
|
||||
}
|
||||
const fields: (keyof ContractQuery)[] = [
|
||||
'contractNo',
|
||||
|
||||
@@ -13,7 +13,7 @@ function unwrapData(res: any) {
|
||||
function mapListQuery(query: TemplateQuery = {}): Record<string, any> {
|
||||
const params: Record<string, any> = {
|
||||
pageNum: query.pageNum ?? 1,
|
||||
pageSize: query.pageSize ?? 10
|
||||
pageSize: query.pageSize ?? 20
|
||||
}
|
||||
;['templateNo', 'templateName', 'templateType', 'beginTime', 'endTime'].forEach((key) => {
|
||||
const val = (query as any)[key]
|
||||
|
||||
@@ -25,7 +25,7 @@ function unwrapData(res: any) {
|
||||
function mapListQuery(query: OpportunityQuery = {}): Record<string, any> {
|
||||
const body: Record<string, any> = {
|
||||
page: query.pageNum ?? 1,
|
||||
pageSize: query.pageSize ?? 10
|
||||
pageSize: query.pageSize ?? 20
|
||||
}
|
||||
if (query.opportunityName) body.opportunityName = query.opportunityName
|
||||
if (query.customerName) body.customerName = query.customerName
|
||||
@@ -177,7 +177,7 @@ export async function listOpportunityProduct(query?: OpportunityProductQuery) {
|
||||
url: `${BASE}/product/list`,
|
||||
data: {
|
||||
page: query?.pageNum ?? 1,
|
||||
pageSize: query?.pageSize ?? 10,
|
||||
pageSize: query?.pageSize ?? 20,
|
||||
opportunityId: query?.opportunityId != null ? String(query.opportunityId) : undefined,
|
||||
productId: query?.productId != null ? String(query.productId) : undefined
|
||||
}
|
||||
@@ -232,7 +232,7 @@ export async function listOpportunityFollow(query?: OpportunityFollowQuery) {
|
||||
url: `${BASE}/follow/list`,
|
||||
data: {
|
||||
page: query?.pageNum ?? 1,
|
||||
pageSize: query?.pageSize ?? 10,
|
||||
pageSize: query?.pageSize ?? 20,
|
||||
opportunityId: query?.opportunityId != null ? String(query.opportunityId) : undefined,
|
||||
followType: query?.followType,
|
||||
followBy: query?.followBy != null ? String(query.followBy) : undefined
|
||||
|
||||
@@ -22,7 +22,7 @@ function unwrapData(res: any) {
|
||||
function mapListQuery(query: QuoteQuery = {}): Record<string, any> {
|
||||
const body: Record<string, any> = {
|
||||
page: query.pageNum ?? 1,
|
||||
pageSize: query.pageSize ?? 10
|
||||
pageSize: query.pageSize ?? 20
|
||||
}
|
||||
const fields: (keyof QuoteQuery)[] = [
|
||||
'keyword',
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function listSupplierInfo(query: SupplierInfoQuery = {}) {
|
||||
url: `${BASE}/list`,
|
||||
data: {
|
||||
page: query.pageNum ?? 1,
|
||||
pageSize: query.pageSize ?? 10,
|
||||
pageSize: query.pageSize ?? 20,
|
||||
supplierName: query.supplierName,
|
||||
status: query.status
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export async function exportSupplierInfo(query: SupplierInfoQuery) {
|
||||
url: `${BASE}/export`,
|
||||
data: {
|
||||
page: query.pageNum ?? 1,
|
||||
pageSize: query.pageSize ?? 10,
|
||||
pageSize: query.pageSize ?? 20,
|
||||
supplierName: query.supplierName,
|
||||
status: query.status
|
||||
},
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** Demo 模板页 API 统一出口(Mock,无后端) */
|
||||
export * from './mock-api'
|
||||
@@ -0,0 +1,389 @@
|
||||
/**
|
||||
* Demo 模板页 Mock API(本地内存,不依赖后端)
|
||||
* 供 views/demo/* 与 product-view-dialog 使用
|
||||
*/
|
||||
import { fetchUserList } from '@/api/admin'
|
||||
import {
|
||||
demoCategories,
|
||||
demoOrders,
|
||||
demoProducts,
|
||||
demoRegions,
|
||||
demoUnits
|
||||
} from './mock-data'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
|
||||
type PageParams = { page?: number; pageSize?: number; keyword?: string }
|
||||
|
||||
const delay = (ms = 120) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
function paginate<T>(list: T[], page = 1, pageSize = DEFAULT_LIST_PAGE_SIZE) {
|
||||
const p = Math.max(1, page)
|
||||
const ps = Math.max(1, pageSize)
|
||||
const start = (p - 1) * ps
|
||||
const slice = list.slice(start, start + ps)
|
||||
return {
|
||||
list: slice,
|
||||
total: list.length,
|
||||
page: p,
|
||||
pageSize: ps,
|
||||
totalPages: Math.max(1, Math.ceil(list.length / ps))
|
||||
}
|
||||
}
|
||||
|
||||
function filterKeyword<T extends Record<string, any>>(
|
||||
list: T[],
|
||||
keys: string[],
|
||||
keyword?: string
|
||||
) {
|
||||
const kw = (keyword || '').trim().toLowerCase()
|
||||
if (!kw) return list
|
||||
return list.filter((row) => keys.some((k) => String(row[k] ?? '').toLowerCase().includes(kw)))
|
||||
}
|
||||
|
||||
const units = [...demoUnits]
|
||||
const categories = [...demoCategories]
|
||||
const products = [...demoProducts]
|
||||
const orders = [...demoOrders]
|
||||
|
||||
function newId(prefix: string) {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
}
|
||||
|
||||
// ─── 物品单位 ─────────────────────────────────────────
|
||||
|
||||
export async function fetchItemUnitList(params?: PageParams) {
|
||||
await delay()
|
||||
const filtered = filterKeyword(units, ['unitName', 'unitCode'], params?.keyword)
|
||||
return paginate(filtered, params?.page, params?.pageSize)
|
||||
}
|
||||
|
||||
export async function fetchCreateItemUnit(data: {
|
||||
unitCode: string
|
||||
unitName: string
|
||||
orderNum?: number
|
||||
remark?: string
|
||||
}) {
|
||||
await delay()
|
||||
const row: Api.ItemUnit.ItemUnitItem = {
|
||||
id: newId('demo-unit'),
|
||||
unitCode: data.unitCode,
|
||||
unitName: data.unitName,
|
||||
tenantNo: 'demo',
|
||||
orderNum: data.orderNum ?? 0,
|
||||
remark: data.remark || '',
|
||||
createTime: new Date().toISOString().slice(0, 19).replace('T', ' '),
|
||||
updateTime: new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
units.unshift(row)
|
||||
return row
|
||||
}
|
||||
|
||||
export async function fetchUpdateItemUnit(
|
||||
id: string,
|
||||
data: Partial<{ unitCode: string; unitName: string; orderNum: number; remark: string }>
|
||||
) {
|
||||
await delay()
|
||||
const idx = units.findIndex((u) => u.id === id)
|
||||
if (idx >= 0) units[idx] = { ...units[idx], ...data }
|
||||
return units[idx]
|
||||
}
|
||||
|
||||
export async function fetchDeleteItemUnit(id: string) {
|
||||
await delay()
|
||||
const idx = units.findIndex((u) => u.id === id)
|
||||
if (idx >= 0) units.splice(idx, 1)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// ─── 分类 ───────────────────────────────────────────
|
||||
|
||||
export async function fetchCategoryList(params?: PageParams & { isActive?: boolean }) {
|
||||
await delay()
|
||||
let list = [...categories]
|
||||
if (params?.isActive != null) list = list.filter((c) => c.isActive === params.isActive)
|
||||
list = filterKeyword(list, ['name'], params?.keyword)
|
||||
return paginate(list, params?.page, params?.pageSize)
|
||||
}
|
||||
|
||||
export async function fetchCreateCategory(data: {
|
||||
name: string
|
||||
parentId?: string
|
||||
sortOrder?: number
|
||||
imageUrl?: string
|
||||
}) {
|
||||
await delay()
|
||||
const row: Api.Category.CategoryItem = {
|
||||
id: newId('demo-cat'),
|
||||
name: data.name,
|
||||
parentId: data.parentId || '0',
|
||||
sortOrder: data.sortOrder ?? 0,
|
||||
isActive: true,
|
||||
imageUrl: data.imageUrl || '',
|
||||
createAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
categories.push(row)
|
||||
return row
|
||||
}
|
||||
|
||||
export async function fetchUpdateCategory(id: string, data: Partial<Api.Category.CategoryItem>) {
|
||||
await delay()
|
||||
const idx = categories.findIndex((c) => c.id === id)
|
||||
if (idx >= 0) categories[idx] = { ...categories[idx], ...data }
|
||||
return categories[idx]
|
||||
}
|
||||
|
||||
export async function fetchDeleteCategory(id: string) {
|
||||
await delay()
|
||||
const idx = categories.findIndex((c) => c.id === id)
|
||||
if (idx >= 0) categories.splice(idx, 1)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchUploadCategoryImage(_data: FormData) {
|
||||
await delay()
|
||||
return { fileUrl: '', fileName: 'demo.png' }
|
||||
}
|
||||
|
||||
export async function fetchProductParameterSimple() {
|
||||
await delay()
|
||||
return { list: [] as Api.Product.ProductParameterSimple[], total: 0 }
|
||||
}
|
||||
|
||||
export async function fetchCategoryParameters(_categoryId: string) {
|
||||
await delay()
|
||||
return { list: [] as Api.Product.CategoryParameter[], total: 0 }
|
||||
}
|
||||
|
||||
export async function fetchUpdateCategoryParameters(_categoryId: string, _items: unknown[]) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// ─── 商品单位(Unit 命名,商品页用) ─────────────────
|
||||
|
||||
export async function fetchUnitList(params?: PageParams) {
|
||||
await delay()
|
||||
const list: Api.Unit.UnitItem[] = units.map((u) => ({
|
||||
id: u.id,
|
||||
name: u.unitName,
|
||||
code: u.unitCode,
|
||||
isActive: true,
|
||||
createAt: u.createTime
|
||||
}))
|
||||
const filtered = filterKeyword(list, ['name', 'code'], params?.keyword)
|
||||
return paginate(filtered, params?.page, params?.pageSize)
|
||||
}
|
||||
|
||||
// ─── 商品 ───────────────────────────────────────────
|
||||
|
||||
export async function fetchProductList(params?: PageParams & { categoryId?: string }) {
|
||||
await delay()
|
||||
let list = [...products]
|
||||
if (params?.categoryId) list = list.filter((p) => p.categoryId === params.categoryId)
|
||||
list = filterKeyword(list, ['name'], params?.keyword)
|
||||
return paginate(list, params?.page, params?.pageSize)
|
||||
}
|
||||
|
||||
export async function fetchProductDetail(id: string) {
|
||||
await delay()
|
||||
const base = products.find((p) => p.id === id)
|
||||
if (!base) throw new Error('商品不存在')
|
||||
return {
|
||||
...base,
|
||||
updateAt: base.createAt,
|
||||
updateBy: 'demo',
|
||||
skuCode: 'SKU-DEMO',
|
||||
measureValue: 1,
|
||||
detailContent: ''
|
||||
} as Api.Product.ProductDetail & { skuCode?: string; measureValue?: number; detailContent?: string }
|
||||
}
|
||||
|
||||
export async function fetchCreateProduct(data: Partial<Api.Product.ProductItem> & { name: string }) {
|
||||
await delay()
|
||||
const row: Api.Product.ProductItem = {
|
||||
id: newId('demo-prod'),
|
||||
name: data.name,
|
||||
categoryId: data.categoryId,
|
||||
categoryName: categories.find((c) => c.id === data.categoryId)?.name,
|
||||
unitId: data.unitId,
|
||||
unitName: units.find((u) => u.id === data.unitId)?.unitName,
|
||||
description: data.description || '',
|
||||
imageUrl: data.imageUrl || '',
|
||||
isActive: data.isActive ?? true,
|
||||
createAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
products.unshift(row)
|
||||
return row
|
||||
}
|
||||
|
||||
export async function fetchUpdateProduct(id: string, data: Partial<Api.Product.ProductItem>) {
|
||||
await delay()
|
||||
const idx = products.findIndex((p) => p.id === id)
|
||||
if (idx >= 0) products[idx] = { ...products[idx], ...data }
|
||||
return products[idx]
|
||||
}
|
||||
|
||||
export async function fetchDeleteProduct(id: string) {
|
||||
await delay()
|
||||
const idx = products.findIndex((p) => p.id === id)
|
||||
if (idx >= 0) products.splice(idx, 1)
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchProductBannerList(_productId: string) {
|
||||
await delay()
|
||||
return { list: [] as Api.Product.ProductBanner[], total: 0 }
|
||||
}
|
||||
|
||||
export async function fetchCreateProductBanners(_productId: string, _items: unknown[]) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchUpdateProductBannerSort(_productId: string, _items: unknown[]) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchDeleteProductBanner(_id: string) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchProductDetailMediaList(_productId: string) {
|
||||
await delay()
|
||||
return { list: [] as Api.Product.ProductDetailMedia[], total: 0 }
|
||||
}
|
||||
|
||||
export async function fetchCreateProductDetailMedia(_productId: string, _items: unknown[]) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchUpdateProductDetailMediaSort(_productId: string, _items: unknown[]) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchDeleteProductDetailMedia(_id: string) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchProductParameterValues(_productId: string) {
|
||||
await delay()
|
||||
return { list: [] as Api.Product.ProductParameterValue[], total: 0 }
|
||||
}
|
||||
|
||||
export async function fetchSaveProductParameterValues(_productId: string, _values: unknown[]) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchProductRegionPrices(_productId: string) {
|
||||
await delay()
|
||||
return {
|
||||
list: demoRegions.map((r) => ({
|
||||
regionId: r.id,
|
||||
regionName: r.name,
|
||||
price: 100,
|
||||
unitId: null,
|
||||
unitName: null
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 区域 / 用户(订单页) ───────────────────────────
|
||||
|
||||
export async function fetchNotDeletedRegions() {
|
||||
await delay()
|
||||
return [...demoRegions]
|
||||
}
|
||||
|
||||
export { fetchUserList }
|
||||
|
||||
// ─── business.ts 订单等 ─────────────────────────────
|
||||
|
||||
export async function fetchOrderList(params?: PageParams & Record<string, unknown>) {
|
||||
await delay()
|
||||
let list = [...orders]
|
||||
list = filterKeyword(list, ['orderNo', 'clientName'], params?.keyword as string | undefined)
|
||||
return paginate(list, params?.page, params?.pageSize)
|
||||
}
|
||||
|
||||
export async function fetchOrderDetail(id: string) {
|
||||
await delay()
|
||||
const base = orders.find((o) => o.id === id)
|
||||
if (!base) throw new Error('订单不存在')
|
||||
return {
|
||||
...base,
|
||||
items: [],
|
||||
remark: 'Demo 订单详情'
|
||||
} as Api.Order.OrderDetail
|
||||
}
|
||||
|
||||
export async function fetchAssignDelivery(_data: unknown) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchReviewCancel(_data: unknown) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchCancelOrder(_data: unknown) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchOrderItems(_orderId: string) {
|
||||
await delay()
|
||||
return { list: [] }
|
||||
}
|
||||
|
||||
export async function fetchAdjustOrder(_data: unknown) {
|
||||
await delay()
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function fetchOrderAdjusts(_orderId: string) {
|
||||
await delay()
|
||||
return { list: [] }
|
||||
}
|
||||
|
||||
export async function fetchPriceVersionDetail(_id: string) {
|
||||
await delay()
|
||||
return { id: _id, name: 'Demo 价目表' }
|
||||
}
|
||||
|
||||
export async function fetchUploadAttachment(_data: FormData) {
|
||||
await delay()
|
||||
return {
|
||||
id: newId('att'),
|
||||
fileName: 'demo.pdf',
|
||||
fileUrl: '#',
|
||||
fileSize: 0
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchUploadProductImage(_data: FormData) {
|
||||
await delay()
|
||||
return { fileUrl: '', fileName: 'demo.png' }
|
||||
}
|
||||
|
||||
export async function fetchSystemConfig() {
|
||||
await delay()
|
||||
return {
|
||||
orderCancelReasons: ['客户取消', '库存不足'],
|
||||
paymentMethods: [
|
||||
{ value: 'cash', label: '现金' },
|
||||
{ value: 'transfer', label: '转账' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAttachmentDownloadUrl(_data: { fileUrl: string; fileName?: string }) {
|
||||
await delay()
|
||||
return { downloadUrl: '#' }
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/** Demo 模板页本地种子数据(无后端依赖) */
|
||||
|
||||
export const demoUnits: Api.ItemUnit.ItemUnitItem[] = [
|
||||
{
|
||||
id: 'demo-unit-1',
|
||||
unitCode: 'KG',
|
||||
unitName: '千克',
|
||||
tenantNo: 'demo',
|
||||
orderNum: 1,
|
||||
remark: 'Demo 示例',
|
||||
createTime: '2026-01-01 10:00:00',
|
||||
updateTime: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 'demo-unit-2',
|
||||
unitCode: 'TON',
|
||||
unitName: '吨',
|
||||
tenantNo: 'demo',
|
||||
orderNum: 2,
|
||||
remark: '',
|
||||
createTime: '2026-01-02 10:00:00',
|
||||
updateTime: '2026-01-02 10:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
export const demoCategories: Api.Category.CategoryItem[] = [
|
||||
{
|
||||
id: 'demo-cat-1',
|
||||
name: '化工原料',
|
||||
parentId: '0',
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
imageUrl: '',
|
||||
createAt: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 'demo-cat-2',
|
||||
name: '树脂类',
|
||||
parentId: 'demo-cat-1',
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
imageUrl: '',
|
||||
createAt: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 'demo-cat-3',
|
||||
name: '溶剂类',
|
||||
parentId: 'demo-cat-1',
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
imageUrl: '',
|
||||
createAt: '2026-01-01 10:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
export const demoProducts: Api.Product.ProductItem[] = [
|
||||
{
|
||||
id: 'demo-prod-1',
|
||||
name: '环氧树脂 E-51',
|
||||
categoryId: 'demo-cat-2',
|
||||
categoryName: '树脂类',
|
||||
unitId: 'demo-unit-1',
|
||||
unitName: '千克',
|
||||
description: 'Demo 商品示例',
|
||||
imageUrl: '',
|
||||
isActive: true,
|
||||
createAt: '2026-01-03 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 'demo-prod-2',
|
||||
name: '丙酮',
|
||||
categoryId: 'demo-cat-3',
|
||||
categoryName: '溶剂类',
|
||||
unitId: 'demo-unit-2',
|
||||
unitName: '吨',
|
||||
description: '',
|
||||
imageUrl: '',
|
||||
isActive: true,
|
||||
createAt: '2026-01-04 10:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
export const demoOrders: Api.Order.OrderItem[] = [
|
||||
{
|
||||
id: 'demo-order-1',
|
||||
orderNo: 'DD202601001',
|
||||
clientId: 'demo-client-1',
|
||||
clientName: '示例客户 A',
|
||||
regionId: 'demo-region-1',
|
||||
regionName: '华东区',
|
||||
priceListId: 'demo-price-1',
|
||||
totalAmount: '12800.00',
|
||||
paidAmount: '12800.00',
|
||||
receivableAmount: '0.00',
|
||||
refundAmount: '0.00',
|
||||
status: 'completed',
|
||||
paymentMethod: 'transfer',
|
||||
contactPerson: '张三',
|
||||
contactPhone: '13800000001',
|
||||
deliveryAddress: '浙江省杭州市示例路 1 号',
|
||||
createAt: '2026-01-10 09:30:00'
|
||||
},
|
||||
{
|
||||
id: 'demo-order-2',
|
||||
orderNo: 'DD202601002',
|
||||
clientId: 'demo-client-2',
|
||||
clientName: '示例客户 B',
|
||||
regionId: 'demo-region-2',
|
||||
regionName: '华南区',
|
||||
priceListId: 'demo-price-1',
|
||||
totalAmount: '5600.00',
|
||||
paidAmount: '0.00',
|
||||
receivableAmount: '5600.00',
|
||||
refundAmount: '0.00',
|
||||
status: 'pending_payment',
|
||||
paymentMethod: 'cash',
|
||||
contactPerson: '李四',
|
||||
contactPhone: '13800000002',
|
||||
deliveryAddress: '广东省广州市示例路 2 号',
|
||||
createAt: '2026-01-11 14:20:00'
|
||||
}
|
||||
]
|
||||
|
||||
export const demoRegions: Api.Region.RegionItem[] = [
|
||||
{
|
||||
id: 'demo-region-1',
|
||||
name: '华东区',
|
||||
code: 'EAST',
|
||||
isActive: true,
|
||||
sortOrder: 1,
|
||||
createAt: '2026-01-01 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 'demo-region-2',
|
||||
name: '华南区',
|
||||
code: 'SOUTH',
|
||||
isActive: true,
|
||||
sortOrder: 2,
|
||||
createAt: '2026-01-01 10:00:00'
|
||||
}
|
||||
]
|
||||
@@ -2,11 +2,18 @@
|
||||
* 运输计划管理 API
|
||||
*/
|
||||
import request from '@/utils/http/bx'
|
||||
import { pickBxList, pickBxPageResult } from '@/utils/http/bx'
|
||||
import type { TransportResourceSnapshot } from '@/utils/pl-transport-resource-snapshot'
|
||||
|
||||
// =============================================================================
|
||||
// 基础数据 API
|
||||
// =============================================================================
|
||||
|
||||
export interface PlListResponse<T> {
|
||||
total: number
|
||||
list: T[]
|
||||
}
|
||||
|
||||
// 承运商
|
||||
export interface Carrier {
|
||||
id: string
|
||||
@@ -32,10 +39,13 @@ export interface CarrierQuery {
|
||||
q?: string
|
||||
status?: string
|
||||
auditStatus?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const carrierApi = {
|
||||
list: (params?: CarrierQuery) => request.get<Carrier[]>('/transport/carrier', { params }),
|
||||
list: (params?: CarrierQuery) =>
|
||||
request.get<PlListResponse<Carrier>>('/transport/carrier', { params }),
|
||||
get: (id: string) => request.get<Carrier>(`/transport/carrier/${id}`),
|
||||
create: (data: Partial<Carrier>) => request.post<Carrier>('/transport/carrier', data),
|
||||
update: (id: string, data: Partial<Carrier>) => request.put<Carrier>(`/transport/carrier/${id}`, data),
|
||||
@@ -116,10 +126,13 @@ export interface VehicleQuery {
|
||||
carrierId?: string
|
||||
vehicleStatus?: string
|
||||
excludeIneligibleCarrier?: boolean
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const vehicleApi = {
|
||||
list: (params?: VehicleQuery) => request.get<Vehicle[]>('/transport/vehicle', { params }),
|
||||
list: (params?: VehicleQuery) =>
|
||||
request.get<PlListResponse<Vehicle>>('/transport/vehicle', { params }),
|
||||
get: (id: string) => request.get<VehicleDetail>(`/transport/vehicle/${id}`),
|
||||
create: (data: VehiclePayload) => request.post<VehicleDetail>('/transport/vehicle', data),
|
||||
update: (id: string, data: Partial<VehiclePayload>) =>
|
||||
@@ -157,10 +170,13 @@ export interface PersonnelPayload {
|
||||
export interface PersonnelQuery {
|
||||
q?: string
|
||||
status?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export const personnelApi = {
|
||||
list: (params?: PersonnelQuery) => request.get<Personnel[]>('/transport/personnel', { params }),
|
||||
list: (params?: PersonnelQuery) =>
|
||||
request.get<PlListResponse<Personnel>>('/transport/personnel', { params }),
|
||||
get: (id: string) => request.get<PersonnelDetail>(`/transport/personnel/${id}`),
|
||||
create: (data: PersonnelPayload) => request.post<PersonnelDetail>('/transport/personnel', data),
|
||||
update: (id: string, data: Partial<PersonnelPayload>) =>
|
||||
@@ -186,6 +202,8 @@ export interface Driver {
|
||||
|
||||
export interface DriverQuery {
|
||||
q?: string
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
function personnelToDriver(p: Personnel): Driver {
|
||||
@@ -211,9 +229,19 @@ function driverToPersonnelPayload(data: Partial<Driver>): PersonnelPayload {
|
||||
|
||||
export const driverApi = {
|
||||
list: async (params?: DriverQuery) => {
|
||||
const res = await personnelApi.list({ q: params?.q })
|
||||
const rows = (res.data ?? res) as Personnel[]
|
||||
return { ...res, data: rows.map(personnelToDriver) }
|
||||
const res = await personnelApi.list({
|
||||
q: params?.q,
|
||||
page: params?.page,
|
||||
pageSize: params?.pageSize
|
||||
})
|
||||
const { list, total } = pickBxPageResult<Personnel>(res)
|
||||
return {
|
||||
...res,
|
||||
data: {
|
||||
total,
|
||||
list: list.map(personnelToDriver)
|
||||
}
|
||||
}
|
||||
},
|
||||
get: async (id: string) => {
|
||||
const res = await personnelApi.get(id)
|
||||
@@ -304,8 +332,11 @@ export interface Plan {
|
||||
destinationNotes?: string
|
||||
vehicleId?: string
|
||||
licensePlate?: string
|
||||
trailerNo?: string
|
||||
driverId?: string
|
||||
driverName?: string
|
||||
personnelSummary?: string
|
||||
resourceSnapshot?: TransportResourceSnapshot
|
||||
transportId?: string
|
||||
transportNo?: string
|
||||
totalAmount: number
|
||||
@@ -455,9 +486,11 @@ export interface Transport {
|
||||
transportNo: string
|
||||
vehicleId?: string
|
||||
licensePlate?: string
|
||||
trailerNo?: string
|
||||
driverId?: string
|
||||
driverName?: string
|
||||
driverPhone?: string
|
||||
resourceSnapshot?: TransportResourceSnapshot
|
||||
carrierId?: string
|
||||
carrierName?: string
|
||||
transportMedium?: string
|
||||
@@ -719,6 +752,7 @@ export const freightApi = {
|
||||
request.post<FreightSettlement>('/transport/freight/', data),
|
||||
update: (id: string, data: Partial<FreightSettlement>) =>
|
||||
request.put<FreightSettlement>(`/transport/freight/${id}`, data),
|
||||
cancel: (id: string) => request.post<FreightSettlement>(`/transport/freight/${id}/cancel`),
|
||||
delete: (id: string) => request.delete(`/transport/freight/${id}`),
|
||||
submitApproval: (id: string, data: FreightSettlementSubmitBody) =>
|
||||
request.post<FreightSettlement>(`/transport/freight/${id}/submit-approval`, data),
|
||||
|
||||
@@ -430,6 +430,232 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// 面板级胶囊分段 Tab(薄荷绿渐变轨道 + 主色选中,对齐 zkxw 生产工单等页)
|
||||
.panel-pill-tabs {
|
||||
// 自定义按钮结构(生产工单 TabBar 等)
|
||||
&:not(.el-tabs) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 12px 24px 4px;
|
||||
}
|
||||
|
||||
&--start:not(.el-tabs) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&--lg:not(.el-tabs) {
|
||||
margin: 16px 24px 8px;
|
||||
|
||||
.panel-pill-tabs__track {
|
||||
gap: 6px;
|
||||
padding: 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.panel-pill-tabs__btn {
|
||||
min-width: 140px;
|
||||
padding: 10px 28px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__track {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
background: linear-gradient(180deg, var(--primary-bg, #ecfdf5) 0%, #fff 100%);
|
||||
border: 1px solid #d1fae5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 108px;
|
||||
padding: 8px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover:not(.is-active) {
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--primary, #10b981);
|
||||
color: #fff;
|
||||
box-shadow: 0 1px 3px rgba(16, 185, 129, 0.28);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 999px;
|
||||
background: #d97706;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
border: 2px solid #d1fae5;
|
||||
pointer-events: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
&__btn.is-active &__badge {
|
||||
border-color: var(--primary, #10b981);
|
||||
}
|
||||
|
||||
&__badge--zero {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
&__badge--dual {
|
||||
min-width: 34px;
|
||||
padding: 0 6px;
|
||||
font-size: 10px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
&__btn.is-active &__badge--zero {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: var(--primary-dark, #059669);
|
||||
border-color: var(--primary, #10b981);
|
||||
}
|
||||
|
||||
// el-tabs 结构
|
||||
&.el-tabs {
|
||||
> .el-tabs__header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 12px 24px 4px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
flex: 0 1 auto;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__nav-scroll {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.el-tabs__nav {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
float: none;
|
||||
padding: 4px;
|
||||
background: linear-gradient(180deg, var(--primary-bg, #ecfdf5) 0%, #fff 100%);
|
||||
border: 1px solid #d1fae5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
box-sizing: border-box;
|
||||
min-width: 108px;
|
||||
height: auto !important;
|
||||
margin: 0 !important;
|
||||
padding: 8px 20px !important;
|
||||
border: none !important;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
text-align: center !important;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
transition:
|
||||
background 0.15s,
|
||||
color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
|
||||
&:hover:not(.is-active) {
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--primary, #10b981);
|
||||
color: #fff;
|
||||
box-shadow: 0 1px 3px rgba(16, 185, 129, 0.28);
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--start.el-tabs {
|
||||
> .el-tabs__header {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.el-tabs__nav-scroll {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&--lg.el-tabs {
|
||||
> .el-tabs__header {
|
||||
margin: 16px 24px 8px;
|
||||
}
|
||||
|
||||
.el-tabs__nav {
|
||||
gap: 6px;
|
||||
padding: 5px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
min-width: 140px;
|
||||
padding: 10px 28px !important;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-container .panel-tabs {
|
||||
border-bottom: 1px solid var(--mint-border);
|
||||
|
||||
@@ -789,7 +1015,8 @@ body {
|
||||
}
|
||||
|
||||
.filter-actions-end,
|
||||
.toolbar-actions {
|
||||
.toolbar-actions,
|
||||
.list-toolbar-end {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -797,6 +1024,44 @@ body {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.list-toolbar-end {
|
||||
flex-shrink: 0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.list-toolbar-end-btn.el-button,
|
||||
.list-toolbar-end-btn.btn-ghost.el-button {
|
||||
--el-button-border-color: transparent;
|
||||
--el-button-hover-border-color: transparent;
|
||||
--el-button-active-border-color: transparent;
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
color: var(--mint-text-muted);
|
||||
border: none !important;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--mint-primary-dark);
|
||||
border: none !important;
|
||||
box-shadow: none;
|
||||
background: var(--mint-primary-bg);
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: none !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.list-toolbar,
|
||||
.toolbar {
|
||||
padding: 0;
|
||||
@@ -819,6 +1084,7 @@ body {
|
||||
.toolbar .el-button + .el-button,
|
||||
.toolbar-right .el-button + .el-button,
|
||||
.toolbar-actions .el-button + .el-button,
|
||||
.list-toolbar-end .el-button + .el-button,
|
||||
.filter-actions-end .el-button + .el-button {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
@@ -858,6 +1124,11 @@ body {
|
||||
|
||||
.pagination.list-pager-bar,
|
||||
.list-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px 12px;
|
||||
margin-top: 0;
|
||||
padding: 10px 16px 12px;
|
||||
border-top: 1px solid var(--mint-border);
|
||||
@@ -865,6 +1136,12 @@ body {
|
||||
border-radius: 0 0 var(--mint-radius) var(--mint-radius);
|
||||
}
|
||||
|
||||
/* 分页条左侧说明文案(BX range / 分类 total-count 等)与右侧控件区分 */
|
||||
.pagination.list-pager-bar .list-range,
|
||||
.pagination.list-pager-bar .total-count {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -1034,6 +1311,26 @@ body {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 标准 CRUD / 大表单弹窗遮罩:顶对齐留白,避免 align-center 高弹窗与顶栏贴齐(protocol-dialog-mask 同构,全局兜底) */
|
||||
.protocol-dialog-mask,
|
||||
.lab-test-item-dialog-mask,
|
||||
.el-overlay:has(.el-dialog.dialog-form-layout),
|
||||
.el-overlay:has(.el-dialog.system-view-dialog),
|
||||
.el-overlay:has(.el-dialog.order-module-dialog) {
|
||||
align-items: flex-start !important;
|
||||
backdrop-filter: none !important;
|
||||
|
||||
.el-overlay-dialog {
|
||||
margin-top: 16px;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.el-dialog.dialog-form-layout,
|
||||
.el-dialog.order-module-dialog {
|
||||
max-width: calc(100vw - 48px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗异步加载:骨架占位,减轻先小后大 */
|
||||
.dialog-scroll.is-view-loading,
|
||||
.dialog-form-body.is-dialog-loading,
|
||||
@@ -1074,7 +1371,22 @@ body {
|
||||
border-bottom: 1px solid var(--mint-border);
|
||||
}
|
||||
|
||||
/* 弹窗 / 抽屉内键值展示:上下布局、黑色文案 */
|
||||
/* Portal 一体化顶栏(对齐 zkxw TOP_LEFT 顶栏 56px + Fresh Mint) */
|
||||
#app-portal-header .portal-header {
|
||||
background: var(--mint-surface) !important;
|
||||
}
|
||||
|
||||
.app-layout-portal #app-worktab .worktab-strip {
|
||||
margin-bottom: 0 !important;
|
||||
padding: 0 24px 4px !important;
|
||||
}
|
||||
|
||||
.app-layout-portal #app-sidebar .layout-sidebar {
|
||||
border-right: 1px solid var(--mint-border) !important;
|
||||
background: var(--mint-surface);
|
||||
}
|
||||
|
||||
/* 弹窗 / 抽屉内键值展示:上下布局、深色正文(对齐 zkxw Token 链) */
|
||||
:is(.el-dialog, .system-view-dialog, .el-drawer) {
|
||||
:is(.view-kv, [class*='-view-kv']:not([class*='-grid']), .pl-plan-kv) {
|
||||
display: flex !important;
|
||||
@@ -1088,14 +1400,14 @@ body {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400 !important;
|
||||
color: #000 !important;
|
||||
color: var(--mint-text-muted) !important;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
color: #000 !important;
|
||||
color: var(--mint-text) !important;
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -1104,7 +1416,7 @@ body {
|
||||
.view-field-label {
|
||||
font-size: 13px;
|
||||
font-weight: 400 !important;
|
||||
color: #000 !important;
|
||||
color: var(--mint-text-muted) !important;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -1122,22 +1434,22 @@ body {
|
||||
|
||||
.el-dialog.dialog-form-layout {
|
||||
.el-form-item__label {
|
||||
color: #000 !important;
|
||||
color: var(--mint-text) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.el-input__inner,
|
||||
.el-textarea__inner,
|
||||
.el-select__selected-item {
|
||||
color: #000 !important;
|
||||
color: var(--mint-text) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.el-input.is-disabled .el-input__inner,
|
||||
.el-input.is-disabled .el-input__wrapper,
|
||||
.el-textarea.is-disabled .el-textarea__inner {
|
||||
color: #000 !important;
|
||||
-webkit-text-fill-color: #000 !important;
|
||||
color: var(--mint-text) !important;
|
||||
-webkit-text-fill-color: var(--mint-text) !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
}
|
||||
@@ -1211,12 +1523,12 @@ body {
|
||||
--el-table-border-color: var(--mint-border);
|
||||
--el-table-row-hover-bg-color: var(--mint-row-hover);
|
||||
--el-table-text-color: var(--mint-text);
|
||||
--el-table-header-text-color: var(--mint-text);
|
||||
--el-table-header-text-color: var(--mint-text-muted);
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
|
||||
th.el-table__cell {
|
||||
color: var(--mint-text);
|
||||
color: var(--mint-text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -1239,21 +1551,144 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局:业务界面文字统一主文本色(消除 muted 灰字) */
|
||||
.text-muted {
|
||||
color: var(--mint-text) !important;
|
||||
}
|
||||
|
||||
html:not(.dark) {
|
||||
.text-g-500,
|
||||
.text-g-600,
|
||||
.text-g-700,
|
||||
.text-g-800 {
|
||||
color: var(--mint-text) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 筛选区 label:13px 次要色(对齐 zkxw filter-form 实践;filter-hint 仍为 11px) */
|
||||
.page-container .filter-block .el-form-item__label,
|
||||
.console-container .filter-block .el-form-item__label,
|
||||
.page-container .filter-form .el-form-item__label,
|
||||
.console-container .filter-form .el-form-item__label {
|
||||
color: var(--mint-text) !important;
|
||||
.console-container .filter-form .el-form-item__label,
|
||||
.el-dialog .filter-block .el-form-item__label,
|
||||
.el-dialog .filter-form .el-form-item__label {
|
||||
font-size: 13px !important;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted) !important;
|
||||
}
|
||||
|
||||
/* label 在上(如 order 筛选) */
|
||||
.page-container .filter-form.el-form--label-top .el-form-item__label,
|
||||
.console-container .filter-form.el-form--label-top .el-form-item__label,
|
||||
.el-dialog .filter-form.el-form--label-top .el-form-item__label {
|
||||
padding: 0 0 4px !important;
|
||||
line-height: 1.2 !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* label 与输入左右布局:label 右侧留间距 */
|
||||
.page-container .filter-form:not(.el-form--label-top) .el-form-item__label,
|
||||
.console-container .filter-form:not(.el-form--label-top) .el-form-item__label,
|
||||
.el-dialog .filter-form:not(.el-form--label-top) .el-form-item__label,
|
||||
.page-container .filter-block:not(:has(.el-form--label-top)) .el-form-item__label,
|
||||
.console-container .filter-block:not(:has(.el-form--label-top)) .el-form-item__label,
|
||||
.el-dialog .filter-block:not(:has(.el-form--label-top)) .el-form-item__label {
|
||||
padding: 0 8px 0 0 !important;
|
||||
line-height: 36px !important;
|
||||
height: 36px !important;
|
||||
}
|
||||
|
||||
/* 筛选栅格:表单项撑满列宽,控件全宽 */
|
||||
.page-container .filter-form,
|
||||
.console-container .filter-form,
|
||||
.el-dialog .filter-form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.page-container .filter-grid .el-form-item,
|
||||
.console-container .filter-grid .el-form-item,
|
||||
.el-dialog .filter-grid .el-form-item {
|
||||
margin-bottom: 0;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-container .filter-form:not(.el-form--label-top) .el-form-item,
|
||||
.console-container .filter-form:not(.el-form--label-top) .el-form-item,
|
||||
.el-dialog .filter-form:not(.el-form--label-top) .el-form-item {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-container .filter-form .el-form-item__content,
|
||||
.console-container .filter-form .el-form-item__content,
|
||||
.el-dialog .filter-form .el-form-item__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.page-container .filter-form .el-form-item__content > .el-input,
|
||||
.page-container .filter-form .el-form-item__content > .el-select,
|
||||
.page-container .filter-form .el-form-item__content > .el-date-editor,
|
||||
.page-container .filter-form .el-form-item__content > .el-tree-select,
|
||||
.page-container .filter-form .el-form-item__content > .el-cascader,
|
||||
.page-container .filter-form .el-form-item__content > .el-input-number,
|
||||
.console-container .filter-form .el-form-item__content > .el-input,
|
||||
.console-container .filter-form .el-form-item__content > .el-select,
|
||||
.console-container .filter-form .el-form-item__content > .el-date-editor,
|
||||
.console-container .filter-form .el-form-item__content > .el-tree-select,
|
||||
.console-container .filter-form .el-form-item__content > .el-cascader,
|
||||
.console-container .filter-form .el-form-item__content > .el-input-number,
|
||||
.el-dialog .filter-form .el-form-item__content > .el-input,
|
||||
.el-dialog .filter-form .el-form-item__content > .el-select,
|
||||
.el-dialog .filter-form .el-form-item__content > .el-date-editor,
|
||||
.el-dialog .filter-form .el-form-item__content > .el-tree-select,
|
||||
.el-dialog .filter-form .el-form-item__content > .el-cascader,
|
||||
.el-dialog .filter-form .el-form-item__content > .el-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-container .filter-form .el-select .el-select__wrapper,
|
||||
.page-container .filter-form .el-tree-select .el-select__wrapper,
|
||||
.console-container .filter-form .el-select .el-select__wrapper,
|
||||
.console-container .filter-form .el-tree-select .el-select__wrapper,
|
||||
.el-dialog .filter-form .el-select .el-select__wrapper,
|
||||
.el-dialog .filter-form .el-tree-select .el-select__wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-container .filter-block .filter-hint,
|
||||
.console-container .filter-block .filter-hint {
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
.list-column-setting-popover.el-popper {
|
||||
padding: 12px 14px 10px;
|
||||
}
|
||||
|
||||
.list-column-setting {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.list-column-setting-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.list-column-setting-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text);
|
||||
}
|
||||
|
||||
.list-column-setting-reset {
|
||||
font-size: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.list-column-setting-check-all {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--mint-border);
|
||||
}
|
||||
|
||||
.list-column-setting-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.list-column-setting-item {
|
||||
display: flex;
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
'white': #ffffff,
|
||||
'black': #000000,
|
||||
'success': (
|
||||
'base': #10b981
|
||||
'base': #059669
|
||||
),
|
||||
'warning': (
|
||||
'base': #ffae1f
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
// 系统主色
|
||||
--main-color: var(--mint-primary);
|
||||
--el-color-primary: var(--mint-primary) !important;
|
||||
--el-color-primary-light-3: #34d399 !important;
|
||||
--el-color-primary-light-5: #6ee7b7 !important;
|
||||
--el-color-primary-light-7: #a7f3d0 !important;
|
||||
--el-color-primary-light-8: #d1fae5 !important;
|
||||
--el-color-primary-light-3: #0d9d6e !important;
|
||||
--el-color-primary-light-5: #34a88a !important;
|
||||
--el-color-primary-light-7: #6bb99a !important;
|
||||
--el-color-primary-light-8: #a8d4c4 !important;
|
||||
--el-color-primary-light-9: var(--mint-primary-bg) !important;
|
||||
--el-color-primary-dark-2: var(--mint-primary-dark) !important;
|
||||
--el-border-color: var(--mint-border) !important;
|
||||
--el-border-color-light: #eaf4ef !important;
|
||||
--el-border-color-light: #dfece6 !important;
|
||||
--el-fill-color-light: var(--mint-table-head) !important;
|
||||
--el-bg-color: var(--mint-surface) !important;
|
||||
--el-bg-color-page: var(--mint-bg-page) !important;
|
||||
@@ -20,7 +20,7 @@
|
||||
// regular 用于大量控件正文(input value / select value / 普通文案)
|
||||
--el-text-color-regular: var(--mint-text) !important;
|
||||
// secondary 用于次级信息(说明/弱提示)
|
||||
--el-text-color-secondary: var(--mint-text) !important;
|
||||
--el-text-color-secondary: var(--mint-text-muted) !important;
|
||||
// placeholder 全局占位符颜色(含弹窗 Teleport 场景)
|
||||
--el-text-color-placeholder: var(--mint-text-placeholder) !important;
|
||||
--el-mask-color: rgba(15, 23, 42, 0.35) !important;
|
||||
@@ -743,13 +743,13 @@ body .el-table thead {
|
||||
--el-table-border-color: var(--mint-border);
|
||||
--el-table-row-hover-bg-color: var(--mint-row-hover);
|
||||
--el-table-text-color: var(--mint-text);
|
||||
--el-table-header-text-color: var(--mint-text);
|
||||
--el-table-header-text-color: var(--mint-text-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page-container .el-table th.el-table__cell,
|
||||
.console-container .el-table th.el-table__cell {
|
||||
color: var(--mint-text);
|
||||
color: var(--mint-text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -1204,13 +1204,27 @@ textarea::placeholder {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
// 列表与分页之间:保留列表底部线,去掉分页顶部线
|
||||
// 列表与分页之间:保留列表底部线,去掉分页顶部线;分页控件右对齐(§7.9)
|
||||
.page-container .pagination.list-pager-bar,
|
||||
.console-container .pagination.list-pager-bar,
|
||||
.el-dialog .pagination.list-pager-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px 12px;
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.page-container .pagination.list-pager-bar .list-range,
|
||||
.page-container .pagination.list-pager-bar .total-count,
|
||||
.console-container .pagination.list-pager-bar .list-range,
|
||||
.console-container .pagination.list-pager-bar .total-count,
|
||||
.el-dialog .pagination.list-pager-bar .list-range,
|
||||
.el-dialog .pagination.list-pager-bar .total-count {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.page-container .el-button.auto-icon-plus,
|
||||
.console-container .el-button.auto-icon-plus {
|
||||
--auto-icon-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'%3E%3Cpath d='M12 5v14M5 12h14'/%3E%3C/svg%3E");
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Fresh Mint 设计令牌(与 test/fresh-mint-green.html、docs/web-admin-ui-spec-fresh-mint.md 一致)
|
||||
// Fresh Mint 设计令牌(与 docs/web-admin-ui-spec-fresh-mint.md 一致;Step1 对齐 zkxw 语义色)
|
||||
:root {
|
||||
--mint-primary: #10b981;
|
||||
--mint-primary-dark: #059669;
|
||||
--mint-primary-bg: #ecfdf5;
|
||||
--mint-text: #1e293b;
|
||||
--mint-text-muted: #64748b;
|
||||
--mint-text-placeholder: #94a3b8;
|
||||
/* 略浅于主文字:label / 说明 / 表头等次要文字,可读且保留层次(对齐 zkxw 实践) */
|
||||
--mint-text-muted: #334155;
|
||||
--mint-text-placeholder: #9ca3af;
|
||||
--mint-border: #d9e8e2;
|
||||
--mint-bg-page: #fefefe;
|
||||
--mint-surface: #ffffff;
|
||||
@@ -15,4 +16,36 @@
|
||||
--mint-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
--mint-radius: 10px;
|
||||
--mint-content-padding: 24px;
|
||||
|
||||
/* 成功 / 启用等语义色(深一档于主色,便于与 primary Tag 区分) */
|
||||
--mint-success: #059669;
|
||||
--mint-success-dark: #047857;
|
||||
--mint-success-bg: #d1fae5;
|
||||
--mint-success-text: #047857;
|
||||
|
||||
--primary: var(--mint-primary);
|
||||
--primary-dark: var(--mint-primary-dark);
|
||||
--primary-bg: var(--mint-primary-bg);
|
||||
|
||||
/* bx/db 模块沿用的语义别名(避免在模块 SCSS 重复 :root) */
|
||||
--text: var(--mint-text);
|
||||
--text-muted: var(--mint-text-muted);
|
||||
--border: var(--mint-border);
|
||||
--bg-page: var(--mint-bg-page);
|
||||
--surface: var(--mint-surface);
|
||||
--table-head: var(--mint-table-head);
|
||||
--row-alt: var(--mint-row-alt);
|
||||
--row-hover: var(--mint-row-hover);
|
||||
--shadow: var(--mint-shadow);
|
||||
--radius: var(--mint-radius);
|
||||
--radius-md: 8px;
|
||||
--control-h: 36px;
|
||||
--success: var(--mint-success);
|
||||
--success-bg: var(--mint-success-bg);
|
||||
--warning: #d97706;
|
||||
--warning-bg: #fffbeb;
|
||||
--danger: #dc2626;
|
||||
--danger-bg: #fef2f2;
|
||||
--info: #64748b;
|
||||
--info-bg: #f8fafc;
|
||||
}
|
||||
@@ -155,7 +155,7 @@ watch(dialogVisible, (val) => {
|
||||
|
||||
const queryParams = ref<ContactQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
customerId: props.customerId,
|
||||
contactName: undefined,
|
||||
mobile: undefined,
|
||||
|
||||
@@ -155,7 +155,7 @@ watch(dialogVisible, (val) => {
|
||||
|
||||
const queryParams = ref<Record<string, any>>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
customerName: undefined,
|
||||
customerNo: undefined,
|
||||
customerType: undefined,
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
|
||||
const props = defineProps<{
|
||||
total: number
|
||||
@@ -33,7 +34,7 @@ const pageModel = computed({
|
||||
})
|
||||
|
||||
const limitModel = computed({
|
||||
get: () => props.limit ?? 10,
|
||||
get: () => props.limit ?? DEFAULT_LIST_PAGE_SIZE,
|
||||
set: (val: number) => emit('update:limit', val)
|
||||
})
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ watch(dialogVisible, (val) => {
|
||||
|
||||
const queryParams = ref<OpportunityQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
opportunityName: undefined,
|
||||
opportunityNo: undefined,
|
||||
customerId: props.customerId,
|
||||
|
||||
@@ -181,7 +181,7 @@ watch(dialogVisible, (val) => {
|
||||
|
||||
const queryParams = ref<QuoteQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
quoteNo: undefined,
|
||||
quoteTitle: undefined,
|
||||
customerName: undefined,
|
||||
|
||||
@@ -114,7 +114,7 @@ watch(dialogVisible, (val) => {
|
||||
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
keyword: undefined as string | undefined
|
||||
})
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ const planCountMap = ref<Record<string, number>>({})
|
||||
|
||||
const queryParams = reactive<ContractQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
contractNo: '',
|
||||
contractName: '',
|
||||
customerName: '',
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
:loading="loading"
|
||||
width="1200px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
/>
|
||||
>
|
||||
<template #hero-actions>
|
||||
<slot name="hero-actions" />
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
:loading="loading"
|
||||
width="1200px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
/>
|
||||
>
|
||||
<template #hero-actions>
|
||||
<slot name="hero-actions" />
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
:loading="loading"
|
||||
width="1200px"
|
||||
modal-class="order-module-dialog-mask"
|
||||
/>
|
||||
>
|
||||
<template #hero-actions>
|
||||
<slot name="hero-actions" />
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<!-- 列表工具栏右侧:刷新 + 列显隐(勾选持久化由 usePersistedListColumns 负责) -->
|
||||
<template>
|
||||
<div class="list-toolbar-end">
|
||||
<el-tooltip content="刷新列表" placement="top">
|
||||
<el-button
|
||||
class="list-toolbar-end-btn"
|
||||
:loading="loading"
|
||||
aria-label="刷新列表"
|
||||
@click="emit('refresh')"
|
||||
>
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-popover
|
||||
v-if="showColumns"
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
:width="300"
|
||||
popper-class="list-column-setting-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button class="list-toolbar-end-btn" aria-label="列设置">
|
||||
<el-icon><Setting /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<div class="list-column-setting">
|
||||
<div class="list-column-setting-head">
|
||||
<span class="list-column-setting-title">列展示</span>
|
||||
<button type="button" class="btn-link list-column-setting-reset" @click="handleReset">
|
||||
恢复默认
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<el-checkbox
|
||||
v-model="checkAllProxy"
|
||||
:indeterminate="isIndeterminate"
|
||||
class="list-column-setting-check-all"
|
||||
>
|
||||
全选
|
||||
</el-checkbox>
|
||||
|
||||
<el-scrollbar max-height="320px" class="list-column-setting-scroll">
|
||||
<div class="list-column-setting-list">
|
||||
<el-checkbox
|
||||
v-for="col in columnDefs"
|
||||
:key="col.prop"
|
||||
:model-value="visibleColumns.includes(col.prop)"
|
||||
:disabled="col.disabled"
|
||||
class="list-column-setting-item"
|
||||
@update:model-value="(val) => handleColumnToggle(col.prop, !!val)"
|
||||
>
|
||||
{{ col.label }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { Refresh, Setting } from '@element-plus/icons-vue'
|
||||
import type { ListColumnDef } from '@/composables/usePersistedListColumns'
|
||||
|
||||
defineOptions({ name: 'ListToolbarEndActions' })
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
loading?: boolean
|
||||
columnDefs?: ListColumnDef[]
|
||||
visibleColumns?: string[]
|
||||
checkAll?: boolean
|
||||
isIndeterminate?: boolean
|
||||
showColumns?: boolean
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
columnDefs: () => [],
|
||||
visibleColumns: () => [],
|
||||
checkAll: false,
|
||||
isIndeterminate: false,
|
||||
showColumns: undefined
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: []
|
||||
'update:visibleColumns': [value: string[]]
|
||||
'update:checkAll': [value: boolean]
|
||||
resetColumns: []
|
||||
}>()
|
||||
|
||||
const showColumns = computed(() => {
|
||||
if (props.showColumns !== undefined) return props.showColumns
|
||||
return props.columnDefs.length > 0
|
||||
})
|
||||
|
||||
const checkAllProxy = computed({
|
||||
get: () => props.checkAll,
|
||||
set: (value: boolean) => emit('update:checkAll', value)
|
||||
})
|
||||
|
||||
const handleColumnToggle = (prop: string, visible: boolean) => {
|
||||
const next = new Set(props.visibleColumns)
|
||||
if (visible) {
|
||||
next.add(prop)
|
||||
} else {
|
||||
next.delete(prop)
|
||||
}
|
||||
const ordered = props.columnDefs
|
||||
.filter((col) => col.disabled || next.has(col.prop))
|
||||
.map((col) => col.prop)
|
||||
emit('update:visibleColumns', ordered)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
emit('resetColumns')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.list-toolbar-end-btn.el-button {
|
||||
--el-button-border-color: transparent;
|
||||
--el-button-hover-border-color: transparent;
|
||||
--el-button-active-border-color: transparent;
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
color: var(--mint-text-muted);
|
||||
border: none !important;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--mint-primary-dark);
|
||||
border: none !important;
|
||||
box-shadow: none;
|
||||
background: var(--mint-primary-bg);
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: none !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,6 +13,9 @@
|
||||
@closed="handleClosed"
|
||||
>
|
||||
<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" />
|
||||
@@ -78,6 +81,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Close } 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'
|
||||
@@ -170,10 +174,10 @@ async function loadDetail(carrierId: string) {
|
||||
try {
|
||||
const [carrierRes, vehicleListRes] = await Promise.all([
|
||||
carrierApi.get(carrierId),
|
||||
vehicleApi.list({ carrierId })
|
||||
vehicleApi.list({ carrierId, page: 1, pageSize: 100 })
|
||||
])
|
||||
carrier.value = ((carrierRes.data ?? carrierRes) || null) as Carrier | null
|
||||
const vehicles = ((vehicleListRes.data ?? vehicleListRes) || []) as { id: string }[]
|
||||
const vehicles = pickBxList<{ id: string }>(vehicleListRes)
|
||||
vehicleBundles.value = await loadVehicleViewBundles(vehicles.map(item => item.id))
|
||||
activeTab.value = vehicleBundles.value[0]?.detail.id || ''
|
||||
} catch {
|
||||
@@ -199,6 +203,16 @@ function handleClosed() {
|
||||
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;
|
||||
|
||||
@@ -81,7 +81,7 @@ function buildCertViewFields(cert: CertRead): ViewField[] {
|
||||
.view-empty-hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #000;
|
||||
color: var(--mint-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,20 @@
|
||||
>
|
||||
<div v-loading="loading" class="view-dialog-body">
|
||||
<div v-if="settlement" class="view-hero">
|
||||
<div class="view-hero-main">
|
||||
<h3 class="view-hero-title">{{ settlement.settlementNo }}</h3>
|
||||
<el-tag :type="getSettlementStatusTagType(settlement.settlementStatus)" size="small">
|
||||
{{ getSettlementStatusLabel(settlement.settlementStatus) }}
|
||||
</el-tag>
|
||||
<div class="view-hero-top">
|
||||
<div class="view-hero-main">
|
||||
<h3 class="view-hero-title">{{ settlement.settlementNo }}</h3>
|
||||
<el-tag :type="getSettlementStatusTagType(settlement.settlementStatus)" size="small">
|
||||
{{ getSettlementStatusLabel(settlement.settlementStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="view-hero-summary">
|
||||
<span class="view-hero-label">运费合计</span>
|
||||
<span class="view-hero-amount">{{ formatAmount(settlement.totalFreight) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="view-hero-summary">
|
||||
<span class="view-hero-label">运费合计</span>
|
||||
<span class="view-hero-amount">{{ formatAmount(settlement.totalFreight) }}</span>
|
||||
<div v-if="$slots['hero-actions']" class="view-hero-actions">
|
||||
<slot name="hero-actions" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -225,9 +230,8 @@ defineExpose({ open })
|
||||
|
||||
.view-hero {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 10px;
|
||||
@@ -235,6 +239,21 @@ defineExpose({ open })
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
.view-hero-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.view-hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.view-hero-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -681,7 +681,7 @@ function resetState() {
|
||||
async function loadMasterData() {
|
||||
const [vehicleRes, carrierRes] = await Promise.all([
|
||||
vehicleApi.list({ vehicleStatus: 'available', excludeIneligibleCarrier: true }),
|
||||
carrierApi.list({ status: '1' })
|
||||
carrierApi.list({ status: '1', page: 1, pageSize: 100 })
|
||||
])
|
||||
vehicleList.value = pickBxList<Vehicle>(vehicleRes)
|
||||
carrierList.value = pickBxList<Carrier>(carrierRes)
|
||||
@@ -792,7 +792,7 @@ async function handleSubmit() {
|
||||
|
||||
const certMessage = buildMissingHazardCertMessage(products.value)
|
||||
if (certMessage) {
|
||||
await ElMessageBox.alert(`${certMessage}。请先上传购买证和运输证。`, '无法安排运输', {
|
||||
await ElMessageBox.alert(`${certMessage}。请先上传购买证和合法使用证明。`, '无法安排运输', {
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
title="易制毒或易制爆产品须上传购买证、运输证;安排运输时将校验是否齐全。"
|
||||
title="易制毒或易制爆产品须上传购买证、合法使用证明;安排运输时将校验是否齐全。"
|
||||
class="cert-tip"
|
||||
/>
|
||||
|
||||
@@ -40,12 +40,12 @@
|
||||
hint="支持图片、PDF 等,上传购买证"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="运输证" required>
|
||||
<el-form-item label="合法使用证明" required>
|
||||
<AttachmentUploadField
|
||||
v-model="item.transportCertUrl"
|
||||
folder="pl/plan-cert"
|
||||
:limit="1"
|
||||
hint="支持图片、PDF 等,上传运输证"
|
||||
hint="支持图片、PDF 等,上传合法使用证明"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
@@ -133,7 +133,7 @@ async function handleSubmit() {
|
||||
p => !p.purchaseCertUrl?.trim() || !p.transportCertUrl?.trim()
|
||||
)
|
||||
if (incomplete.length) {
|
||||
ElMessage.warning('请为每个危化品产品上传购买证和运输证')
|
||||
ElMessage.warning('请为每个危化品产品上传购买证和合法使用证明')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -793,7 +793,7 @@ async function loadMasterData() {
|
||||
try {
|
||||
const [vehicleRes, carrierRes] = await Promise.all([
|
||||
vehicleApi.list({ vehicleStatus: 'available', excludeIneligibleCarrier: true }),
|
||||
carrierApi.list({ status: '1' })
|
||||
carrierApi.list({ status: '1', page: 1, pageSize: 100 })
|
||||
])
|
||||
vehicleList.value = (vehicleRes.data ?? vehicleRes) || []
|
||||
carrierList.value = (carrierRes.data ?? carrierRes) || []
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
disabled
|
||||
/>
|
||||
<span v-else class="pl-plan-list-cert-cell__missing">未上传</span>
|
||||
<span class="pl-plan-list-cert-cell__cert-label">运输证</span>
|
||||
<span class="pl-plan-list-cert-cell__cert-label">合法使用证明</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
</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" />
|
||||
|
||||
<DbPoundSlipQrcodeDialog
|
||||
@@ -174,6 +178,15 @@ function handleClosed() {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.pl-plan-view-hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pl-plan-arrange-summary {
|
||||
margin-bottom: 12px;
|
||||
padding: 12px 16px;
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pl-plan-kv">
|
||||
<dt>运输证</dt>
|
||||
<dt>合法使用证明</dt>
|
||||
<dd>
|
||||
<AttachmentUploadField
|
||||
v-if="item.transportCertUrl"
|
||||
@@ -781,14 +781,14 @@
|
||||
.pl-plan-form-empty-hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
.pl-plan-view-remark {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
color: var(--mint-text);
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
@@ -810,7 +810,7 @@
|
||||
dt {
|
||||
margin: 0 0 4px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
dd {
|
||||
@@ -943,7 +943,7 @@
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
.pl-plan-cert-block {
|
||||
@@ -1010,7 +1010,7 @@
|
||||
|
||||
.pl-plan-site-feedback-batch {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
.pl-plan-site-feedback-attachments {
|
||||
@@ -1042,7 +1042,7 @@
|
||||
margin: 12px 0 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
color: var(--mint-text-muted);
|
||||
}
|
||||
|
||||
.pl-plan-dibang-pending {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -69,15 +69,15 @@
|
||||
dt {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: #000;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
color: var(--mint-text);
|
||||
word-break: break-word;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -403,7 +403,7 @@
|
||||
.view-hero-meta {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #000;
|
||||
color: var(--mint-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -429,8 +429,8 @@
|
||||
.view-field-label {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: #000;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@@ -459,7 +459,7 @@
|
||||
.view-field-group .view-empty-hint {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #000;
|
||||
color: var(--mint-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
height: 100vh;
|
||||
user-select: none;
|
||||
scrollbar-width: none;
|
||||
border-right: none;
|
||||
border-right: 1px solid var(--mint-border, #d9e8e2);
|
||||
background: var(--mint-surface, #fff);
|
||||
|
||||
&.layout-sidebar-portal {
|
||||
@@ -106,18 +106,18 @@
|
||||
overflow: hidden;
|
||||
background: var(--mint-surface, #fff);
|
||||
|
||||
// 长菜单名完整展示:侧栏宽度不变,内容区可横向滚动
|
||||
// 长菜单名在侧栏内省略展示(对齐 zkxw Fresh Mint 侧栏)
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
overflow-x: auto !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__view) {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__bar.is-horizontal) {
|
||||
height: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (width <= 640px) {
|
||||
|
||||
@@ -10,16 +10,23 @@ $popup-menu-padding: 8px;
|
||||
$popup-menu-margin: 5px;
|
||||
$popup-menu-radius: 6px;
|
||||
|
||||
// 通用菜单项样式(min-width 保证短标题仍铺满侧栏;width: max-content 允许长标题撑开以配合横向滚动)
|
||||
// 通用菜单项样式(宽度铺满侧栏,长标题省略)
|
||||
@mixin menu-item-base {
|
||||
width: max-content;
|
||||
min-width: calc(100% - 12px);
|
||||
margin-left: 6px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-left: 0;
|
||||
border-radius: 8px;
|
||||
|
||||
.menu-icon {
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.menu-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// 通用 hover 样式
|
||||
@@ -31,17 +38,26 @@ $popup-menu-radius: 6px;
|
||||
}
|
||||
|
||||
// 通用选中样式
|
||||
@mixin menu-active($color, $bg-color, $icon-color: var(--theme-color)) {
|
||||
@mixin menu-active($color, $bg-color, $icon-color: $color) {
|
||||
.el-menu-item.is-active {
|
||||
color: $color !important;
|
||||
background-color: $bg-color;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
background-color: $bg-color !important;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
.art-svg-icon {
|
||||
color: $icon-color !important;
|
||||
}
|
||||
}
|
||||
|
||||
span,
|
||||
.menu-name {
|
||||
color: $color !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,16 +123,8 @@ $popup-menu-radius: 6px;
|
||||
// 菜单折叠样式
|
||||
.menu-left-close {
|
||||
.header {
|
||||
.header-brand {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-brand-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,28 +157,44 @@ $popup-menu-radius: 6px;
|
||||
// 菜单高度
|
||||
.el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
position: relative;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
height: $menu-height !important;
|
||||
margin-bottom: 2px;
|
||||
line-height: $menu-height !important;
|
||||
overflow: visible !important;
|
||||
overflow: hidden !important;
|
||||
|
||||
.menu-title-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
.menu-name {
|
||||
flex-shrink: 0;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
font-size: $menu-font-size !important;
|
||||
font-weight: 500;
|
||||
// Fresh Mint:菜单文字使用主文本色
|
||||
color: var(--mint-text, #1e293b);
|
||||
// Fresh Mint:默认菜单项为中性色(demo `.nav a`)
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
text-rendering: geometricprecision;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.el-sub-menu > .el-sub-menu__title {
|
||||
font-size: 12px !important;
|
||||
font-weight: 700;
|
||||
color: var(--mint-text, #1e293b) !important;
|
||||
color: var(--mint-text-muted, #64748b) !important;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
@@ -184,46 +208,48 @@ $popup-menu-radius: 6px;
|
||||
.el-sub-menu .el-menu {
|
||||
margin: 2px 0 6px 10px;
|
||||
padding: 2px 0 2px 8px !important;
|
||||
border-left: 2px solid var(--mint-border, #d9e8e2);
|
||||
border-left: 2px solid var(--mint-border, #c5e0d4);
|
||||
}
|
||||
|
||||
// 菜单折叠
|
||||
.el-menu--collapse {
|
||||
.el-sub-menu.is-active {
|
||||
.el-sub-menu__title {
|
||||
background-color: var(--primary, #10b981);
|
||||
|
||||
.menu-icon {
|
||||
.art-svg-icon {
|
||||
// 选中菜单图标颜色
|
||||
color: var(--theme-color) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background-color: var(--primary, #10b981);
|
||||
color: #fff !important;
|
||||
|
||||
span,
|
||||
.menu-name,
|
||||
.menu-icon .art-svg-icon {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------- Design theme menu ----------------------
|
||||
.el-menu-design {
|
||||
@include theme-menu-base;
|
||||
@include menu-active(
|
||||
var(--mint-primary-dark, #059669),
|
||||
var(--mint-primary-bg, #ecfdf5),
|
||||
var(--mint-primary-dark, #059669)
|
||||
);
|
||||
@include menu-active(#fff, var(--primary, #10b981), #fff);
|
||||
@include menu-hover($hover-bg-color);
|
||||
|
||||
.el-menu-item,
|
||||
.el-sub-menu__title {
|
||||
color: var(--mint-text, #1e293b) !important;
|
||||
}
|
||||
|
||||
// 选中态:以浅底+图标强化为主,文字保持中性色(对齐 fresh-mint demo)
|
||||
.el-menu-item.is-active span {
|
||||
color: var(--mint-primary-dark, #059669) !important;
|
||||
font-weight: 600 !important;
|
||||
color: var(--mint-text-muted, #334155) !important;
|
||||
}
|
||||
|
||||
.el-sub-menu__icon-arrow {
|
||||
color: var(--mint-text);
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,24 +274,25 @@ $popup-menu-radius: 6px;
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
background-color: var(--mint-primary-bg, #ecfdf5);
|
||||
color: var(--mint-primary-dark, #059669) !important;
|
||||
background-color: var(--primary, #10b981);
|
||||
color: #fff !important;
|
||||
font-weight: 600;
|
||||
|
||||
span {
|
||||
color: var(--mint-primary-dark, #059669) !important;
|
||||
span,
|
||||
.menu-name {
|
||||
color: #fff !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.art-svg-icon {
|
||||
color: var(--mint-primary-dark, #059669) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
@include menu-hover($hover-bg-color);
|
||||
|
||||
.el-sub-menu__icon-arrow {
|
||||
color: var(--mint-text);
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -290,6 +317,22 @@ $popup-menu-radius: 6px;
|
||||
@include popup-menu-base(var(--art-gray-200), var(--art-gray-900), var(--art-gray-200));
|
||||
}
|
||||
|
||||
.menu-left-design-popper,
|
||||
.menu-left-light-popper {
|
||||
.el-menu--popup {
|
||||
.el-menu-item.is-active {
|
||||
color: #fff !important;
|
||||
background-color: var(--primary, #10b981) !important;
|
||||
|
||||
span,
|
||||
.menu-name,
|
||||
.art-svg-icon {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗黑模式菜单样式
|
||||
.dark {
|
||||
.el-menu--vertical,
|
||||
@@ -304,8 +347,19 @@ $popup-menu-radius: 6px;
|
||||
color: var(--art-gray-800) !important;
|
||||
}
|
||||
|
||||
// 选中的文字颜色跟图标颜色
|
||||
.el-menu-item.is-active {
|
||||
// 选中的文字颜色跟图标颜色(design / light 为深绿底白字)
|
||||
.el-menu-design,
|
||||
.el-menu-light {
|
||||
.el-menu-item.is-active {
|
||||
span,
|
||||
.menu-name,
|
||||
.menu-icon .art-svg-icon {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-dark .el-menu-item.is-active {
|
||||
span,
|
||||
.menu-icon .art-svg-icon {
|
||||
color: var(--theme-color) !important;
|
||||
|
||||
@@ -49,22 +49,25 @@
|
||||
.portal-header {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
padding: 0 20px;
|
||||
height: 56px;
|
||||
padding: 0 var(--mint-content-padding, 24px);
|
||||
user-select: none;
|
||||
background: var(--default-bg-color, #fff);
|
||||
border-bottom: 1px solid var(--art-card-border, #d9e8e2);
|
||||
background: var(--mint-surface, #fff);
|
||||
border-bottom: 1px solid var(--mint-border, #d9e8e2);
|
||||
}
|
||||
|
||||
.portal-header-brand {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-right: 12px;
|
||||
padding-right: 16px;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid var(--mint-border, #d9e8e2);
|
||||
}
|
||||
|
||||
.portal-header-logo {
|
||||
@@ -72,18 +75,19 @@
|
||||
}
|
||||
|
||||
.portal-header-title {
|
||||
max-width: 180px;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
color: var(--mint-text, #1e293b);
|
||||
white-space: nowrap;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.portal-header-nav {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: 24px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.portal-header-actions {
|
||||
|
||||
@@ -95,9 +95,9 @@
|
||||
.portal-top-nav-list {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
min-height: 48px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
@@ -106,11 +106,13 @@
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 72px;
|
||||
padding: 8px 12px;
|
||||
min-width: 64px;
|
||||
height: 48px;
|
||||
padding: 4px 10px;
|
||||
margin: 0 2px;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -120,27 +122,24 @@
|
||||
color 0.2s ease;
|
||||
|
||||
&__icon {
|
||||
font-size: 22px;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
&__label {
|
||||
max-width: 88px;
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
color: var(--mint-text, #1e293b);
|
||||
font-size: 13px;
|
||||
line-height: 1.2;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover:not(.is-active) {
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
|
||||
.portal-top-nav-card__icon,
|
||||
.portal-top-nav-card__label {
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
@@ -149,6 +148,7 @@
|
||||
.portal-top-nav-card__icon,
|
||||
.portal-top-nav-card__label {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
<style scoped>
|
||||
.worktab-strip {
|
||||
position: relative;
|
||||
padding: 0 24px 12px !important;
|
||||
padding: 0 24px 4px !important;
|
||||
background: var(--mint-surface);
|
||||
overflow: visible !important;
|
||||
}
|
||||
@@ -495,7 +495,7 @@
|
||||
z-index: 1;
|
||||
max-width: 220px;
|
||||
padding: 10px 16px !important;
|
||||
margin-bottom: -11px;
|
||||
margin-bottom: -7px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div
|
||||
class="filter-date-shortcuts list-toolbar-date-shortcuts"
|
||||
role="group"
|
||||
:aria-label="ariaLabel"
|
||||
>
|
||||
<el-button
|
||||
v-for="item in shortcutItems"
|
||||
:key="item.type"
|
||||
size="small"
|
||||
class="btn-ghost date-shortcut-btn"
|
||||
:class="{ 'is-active': active === item.type }"
|
||||
@click="emit('change', item.type)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DateShortcutType } from '@/composables/useListDateShortcut'
|
||||
|
||||
defineProps<{
|
||||
active: DateShortcutType
|
||||
ariaLabel?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [type: DateShortcutType]
|
||||
}>()
|
||||
|
||||
const shortcutItems: Array<{ type: DateShortcutType; label: string }> = [
|
||||
{ type: 'all', label: '全部' },
|
||||
{ type: 'yesterday', label: '昨日' },
|
||||
{ type: 'today', label: '今日' },
|
||||
{ type: 'tomorrow', label: '明日' },
|
||||
{ type: 'week', label: '本周' }
|
||||
]
|
||||
</script>
|
||||
@@ -169,7 +169,7 @@
|
||||
// 默认分页常量
|
||||
const DEFAULT_PAGINATION_OPTIONS: PaginationOptions = {
|
||||
pageSizes: [10, 20, 30, 50, 100],
|
||||
align: 'center',
|
||||
align: 'right',
|
||||
background: true,
|
||||
layout: layout.value,
|
||||
hideOnSinglePage: false,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
export type DateShortcutType = 'all' | 'today' | 'tomorrow' | 'yesterday' | 'week'
|
||||
|
||||
function formatYmd(date: Date): string {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表工具栏计划日期快捷筛选(全部 / 昨日 / 今日 / 明日 / 本周)
|
||||
*/
|
||||
export function useListDateShortcut(dateRange: Ref<string[]>, onSearch: () => void) {
|
||||
const activeDateShortcut = ref<DateShortcutType>('all')
|
||||
|
||||
function handleDateShortcut(type: DateShortcutType) {
|
||||
activeDateShortcut.value = type
|
||||
const today = new Date()
|
||||
|
||||
if (type === 'all') {
|
||||
dateRange.value = []
|
||||
} else if (type === 'today') {
|
||||
const d = formatYmd(today)
|
||||
dateRange.value = [d, d]
|
||||
} else if (type === 'tomorrow') {
|
||||
const t = new Date(today)
|
||||
t.setDate(t.getDate() + 1)
|
||||
const d = formatYmd(t)
|
||||
dateRange.value = [d, d]
|
||||
} else if (type === 'yesterday') {
|
||||
const y = new Date(today)
|
||||
y.setDate(y.getDate() - 1)
|
||||
const d = formatYmd(y)
|
||||
dateRange.value = [d, d]
|
||||
} else if (type === 'week') {
|
||||
const start = new Date(today)
|
||||
const day = start.getDay() || 7
|
||||
start.setDate(start.getDate() - day + 1)
|
||||
const end = new Date(start)
|
||||
end.setDate(start.getDate() + 6)
|
||||
dateRange.value = [formatYmd(start), formatYmd(end)]
|
||||
}
|
||||
|
||||
onSearch()
|
||||
}
|
||||
|
||||
function resetDateShortcut() {
|
||||
activeDateShortcut.value = 'all'
|
||||
dateRange.value = []
|
||||
}
|
||||
|
||||
return { activeDateShortcut, handleDateShortcut, resetDateShortcut }
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
export interface ListColumnDef {
|
||||
prop: string
|
||||
label: string
|
||||
/** 默认是否显示;未指定时为 true */
|
||||
defaultVisible?: boolean
|
||||
/** 为 true 时不可取消勾选(如固定展示列) */
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export interface UsePersistedListColumnsOptions {
|
||||
/** 默认可见列 prop 列表;未指定时取 defaultVisible !== false 的列 */
|
||||
defaultVisibleProps?: string[]
|
||||
}
|
||||
|
||||
function resolveDefaultVisibleProps(
|
||||
columns: ListColumnDef[],
|
||||
options?: UsePersistedListColumnsOptions
|
||||
): string[] {
|
||||
if (options?.defaultVisibleProps?.length) {
|
||||
return [...options.defaultVisibleProps]
|
||||
}
|
||||
return columns.filter((col) => col.defaultVisible !== false).map((col) => col.prop)
|
||||
}
|
||||
|
||||
function loadVisibleColumns(
|
||||
storageKey: string,
|
||||
columns: ListColumnDef[],
|
||||
defaultVisibleProps: string[]
|
||||
): string[] {
|
||||
const allowed = new Set(columns.map((col) => col.prop))
|
||||
if (typeof localStorage === 'undefined') {
|
||||
return defaultVisibleProps.filter((prop) => allowed.has(prop))
|
||||
}
|
||||
const saved = localStorage.getItem(storageKey)
|
||||
if (!saved) {
|
||||
return defaultVisibleProps.filter((prop) => allowed.has(prop))
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(saved)
|
||||
if (!Array.isArray(parsed)) {
|
||||
return defaultVisibleProps.filter((prop) => allowed.has(prop))
|
||||
}
|
||||
return parsed.filter((prop): prop is string => typeof prop === 'string' && allowed.has(prop))
|
||||
} catch {
|
||||
return defaultVisibleProps.filter((prop) => allowed.has(prop))
|
||||
}
|
||||
}
|
||||
|
||||
function persistVisibleColumns(storageKey: string, visibleColumns: string[]) {
|
||||
if (typeof localStorage === 'undefined') return
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(visibleColumns))
|
||||
} catch {
|
||||
// ignore quota / private mode errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表可选列显隐 + localStorage 持久化。
|
||||
* 勾选变化会自动写入 storageKey 对应键。
|
||||
*/
|
||||
export function usePersistedListColumns(
|
||||
storageKey: string,
|
||||
columns: ListColumnDef[],
|
||||
options?: UsePersistedListColumnsOptions
|
||||
) {
|
||||
const columnDefs = columns
|
||||
const defaultVisibleProps = resolveDefaultVisibleProps(columns, options)
|
||||
const visibleColumns = ref<string[]>(
|
||||
loadVisibleColumns(storageKey, columns, defaultVisibleProps)
|
||||
)
|
||||
|
||||
watch(
|
||||
visibleColumns,
|
||||
(value) => {
|
||||
persistVisibleColumns(storageKey, value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const toggleableColumns = computed(() => columnDefs.filter((col) => !col.disabled))
|
||||
|
||||
const checkAll = computed({
|
||||
get() {
|
||||
const toggleable = toggleableColumns.value
|
||||
if (!toggleable.length) return false
|
||||
return toggleable.every((col) => visibleColumns.value.includes(col.prop))
|
||||
},
|
||||
set(checked: boolean) {
|
||||
visibleColumns.value = checked
|
||||
? toggleableColumns.value.map((col) => col.prop)
|
||||
: columnDefs.filter((col) => col.disabled).map((col) => col.prop)
|
||||
}
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(() => {
|
||||
const toggleable = toggleableColumns.value
|
||||
if (!toggleable.length) return false
|
||||
const checkedCount = toggleable.filter((col) =>
|
||||
visibleColumns.value.includes(col.prop)
|
||||
).length
|
||||
return checkedCount > 0 && checkedCount < toggleable.length
|
||||
})
|
||||
|
||||
const isColumnVisible = (prop: string) => visibleColumns.value.includes(prop)
|
||||
|
||||
const setColumnVisible = (prop: string, visible: boolean) => {
|
||||
const target = columnDefs.find((col) => col.prop === prop)
|
||||
if (!target || target.disabled) return
|
||||
const next = new Set(visibleColumns.value)
|
||||
if (visible) {
|
||||
next.add(prop)
|
||||
} else {
|
||||
next.delete(prop)
|
||||
}
|
||||
visibleColumns.value = columnDefs
|
||||
.filter((col) => col.disabled || next.has(col.prop))
|
||||
.map((col) => col.prop)
|
||||
}
|
||||
|
||||
const resetToDefault = () => {
|
||||
visibleColumns.value = [...defaultVisibleProps]
|
||||
}
|
||||
|
||||
return {
|
||||
columnDefs,
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
setColumnVisible,
|
||||
resetToDefault
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@/api/pl'
|
||||
import type { PlanFormProductRow } from '@/composables/usePlContractToPlan'
|
||||
import { loadVehicleViewBundle, type VehicleViewBundle } from '@/composables/usePlVehicleViewDetail'
|
||||
import { snapshotToVehicleViewBundle } from '@/utils/pl-transport-resource-snapshot'
|
||||
import { enrichPlanProductsHazardFromCrm } from '@/composables/usePlToxicProductCertCheck'
|
||||
import { pickBxData } from '@/utils/http/bx'
|
||||
|
||||
@@ -235,7 +236,9 @@ export function usePlPlanViewDetail() {
|
||||
)
|
||||
}
|
||||
|
||||
if (data?.vehicleId) {
|
||||
if (data?.resourceSnapshot) {
|
||||
vehicleBundle.value = snapshotToVehicleViewBundle(data.resourceSnapshot)
|
||||
} else if (data?.vehicleId) {
|
||||
detailTasks.push(
|
||||
loadVehicleViewBundle(data.vehicleId)
|
||||
.then((bundle) => {
|
||||
|
||||
@@ -108,5 +108,5 @@ export function buildMissingHazardCertMessage(products: PlanProduct[]) {
|
||||
const missing = findMissingHazardCerts(products)
|
||||
if (!missing.length) return ''
|
||||
const names = missing.map(p => `「${p.productName}」`).join('、')
|
||||
return `以下产品须上传购买证和运输证:${names}`
|
||||
return `以下产品须上传购买证和合法使用证明:${names}`
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { computed, ref, type ComputedRef, type Ref } from 'vue'
|
||||
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
|
||||
type Source<T> = Ref<T[]> | ComputedRef<T[]> | (() => T[])
|
||||
|
||||
interface UsePagedTableOptions {
|
||||
@@ -9,7 +11,7 @@ interface UsePagedTableOptions {
|
||||
|
||||
export function usePagedTable<T>(source: Source<T>, options: UsePagedTableOptions = {}) {
|
||||
const currentPage = ref(options.initialPage ?? 1)
|
||||
const pageSize = ref(options.initialPageSize ?? 10)
|
||||
const pageSize = ref(options.initialPageSize ?? DEFAULT_LIST_PAGE_SIZE)
|
||||
|
||||
const sourceRows = computed(() => {
|
||||
if (typeof source === 'function') return source()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import { DEFAULT_LIST_PAGE_SIZE } from '@/utils/table/tableConfig'
|
||||
|
||||
interface UseServerPagedOptions {
|
||||
initialPage?: number
|
||||
initialPageSize?: number
|
||||
@@ -11,7 +13,7 @@ export function useServerPaged<T>(
|
||||
options: UseServerPagedOptions = {},
|
||||
) {
|
||||
const currentPage = ref(options.initialPage ?? 1)
|
||||
const pageSize = ref(options.initialPageSize ?? 10)
|
||||
const pageSize = ref(options.initialPageSize ?? DEFAULT_LIST_PAGE_SIZE)
|
||||
const total = ref(0)
|
||||
const rows: Ref<T[]> = ref([]) as Ref<T[]>
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
createSmartDebounce,
|
||||
createErrorHandler
|
||||
} from '../../utils/table/tableUtils'
|
||||
import { tableConfig } from '../../utils/table/tableConfig'
|
||||
import { tableConfig, DEFAULT_LIST_PAGE_SIZE } from '../../utils/table/tableConfig'
|
||||
|
||||
// 类型推导工具类型
|
||||
type InferApiParams<T> = T extends (params: infer P) => any ? P : never
|
||||
@@ -205,7 +205,7 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
||||
Object.assign(
|
||||
{
|
||||
[pageKey]: 1,
|
||||
[sizeKey]: 10
|
||||
[sizeKey]: DEFAULT_LIST_PAGE_SIZE
|
||||
},
|
||||
apiParams || {}
|
||||
) as TParams
|
||||
@@ -214,7 +214,7 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
||||
// 分页配置
|
||||
const pagination = reactive<Api.Common.PaginationParams>({
|
||||
current: ((searchParams as Record<string, unknown>)[pageKey] as number) || 1,
|
||||
size: ((searchParams as Record<string, unknown>)[sizeKey] as number) || 10,
|
||||
size: ((searchParams as Record<string, unknown>)[sizeKey] as number) || DEFAULT_LIST_PAGE_SIZE,
|
||||
total: 0
|
||||
})
|
||||
|
||||
@@ -394,7 +394,7 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
||||
if (err instanceof Error && err.message === '请求已取消') {
|
||||
// 请求被取消,回到 idle 状态
|
||||
loadingState.value = 'idle'
|
||||
return { records: [], total: 0, current: 1, size: 10 }
|
||||
return { records: [], total: 0, current: 1, size: DEFAULT_LIST_PAGE_SIZE }
|
||||
}
|
||||
|
||||
// 状态机:请求失败,进入 error 状态
|
||||
@@ -448,7 +448,7 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
||||
const paramsRecord = searchParams as Record<string, unknown>
|
||||
const defaultPagination = {
|
||||
[pageKey]: 1,
|
||||
[sizeKey]: (paramsRecord[sizeKey] as number) || 10
|
||||
[sizeKey]: (paramsRecord[sizeKey] as number) || DEFAULT_LIST_PAGE_SIZE
|
||||
}
|
||||
|
||||
// 清空所有搜索参数
|
||||
@@ -482,7 +482,7 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
|
||||
// 替换搜索参数:适用于表单查询,避免旧字段残留
|
||||
const replaceSearchParams = (params?: Partial<TParams>): void => {
|
||||
const paramsRecord = searchParams as Record<string, unknown>
|
||||
const currentSize = pagination.size || ((paramsRecord[sizeKey] as number) ?? 10)
|
||||
const currentSize = pagination.size || ((paramsRecord[sizeKey] as number) ?? DEFAULT_LIST_PAGE_SIZE)
|
||||
|
||||
Object.keys(searchParams).forEach((key) => {
|
||||
if (key !== pageKey && key !== sizeKey) {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { AppRouteRecord } from '@/types/router'
|
||||
|
||||
/**
|
||||
* Demo 模板页(Fresh Mint UI 参考)
|
||||
* Hash:/#/demo/console、/#/demo/unit 等
|
||||
*/
|
||||
export const demoRoutes: AppRouteRecord[] = [
|
||||
{
|
||||
id: 'DEMO00000000000000000000000001',
|
||||
name: 'DemoGroup',
|
||||
path: '/demo',
|
||||
component: '/index/index',
|
||||
meta: {
|
||||
title: 'Demo',
|
||||
icon: 'ri:palette-line',
|
||||
keepAlive: false,
|
||||
roles: ['admin']
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'DEMO00000000000000000000000002',
|
||||
name: 'DemoConsole',
|
||||
path: 'console',
|
||||
component: '/dashboard/console/index',
|
||||
meta: {
|
||||
title: '工作台',
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'DEMO00000000000000000000000003',
|
||||
name: 'DemoUnit',
|
||||
path: 'unit',
|
||||
component: '/demo/unit/index',
|
||||
meta: {
|
||||
title: '标准列表(单位)',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'DEMO00000000000000000000000004',
|
||||
name: 'DemoOrder',
|
||||
path: 'order',
|
||||
component: '/demo/order/index',
|
||||
meta: {
|
||||
title: '复杂列表(订单)',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'DEMO00000000000000000000000005',
|
||||
name: 'DemoProduct',
|
||||
path: 'product',
|
||||
component: '/demo/product/index',
|
||||
meta: {
|
||||
title: '商品列表',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'DEMO00000000000000000000000006',
|
||||
name: 'DemoCategory',
|
||||
path: 'category',
|
||||
component: '/demo/category/index',
|
||||
meta: {
|
||||
title: '树形主数据(分类)',
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'DEMO00000000000000000000000007',
|
||||
name: 'DemoUser',
|
||||
path: 'user',
|
||||
component: '/system/user/index',
|
||||
meta: {
|
||||
title: '用户 CRUD',
|
||||
keepAlive: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -27,6 +27,7 @@
|
||||
* - 固定标签页保护(不可关闭)
|
||||
* - KeepAlive 缓存排除管理
|
||||
* - 路由有效性验证
|
||||
* - 同时最多打开 10 个标签(超出时自动关闭最早的可关闭标签)
|
||||
* - 首页自动保留
|
||||
*
|
||||
* ## 持久化
|
||||
@@ -52,6 +53,9 @@ interface WorktabState {
|
||||
keepAliveExclude: string[]
|
||||
}
|
||||
|
||||
/** 同时打开的工作标签页数量上限(固定标签不计入淘汰,但计入总数) */
|
||||
const MAX_OPEN_TABS = 10
|
||||
|
||||
/**
|
||||
* 工作台标签页管理 Store
|
||||
*/
|
||||
@@ -167,7 +171,11 @@ export const useWorktabStore = defineStore(
|
||||
}
|
||||
|
||||
if (existingIndex === -1) {
|
||||
// 新增标签页
|
||||
// 新增标签页:超出上限时淘汰最早的可关闭标签
|
||||
if (!ensureCapacityForNewTab()) {
|
||||
return
|
||||
}
|
||||
|
||||
const insertIndex = tab.fixedTab ? findFixedTabInsertIndex() : opened.value.length
|
||||
const newTab = { ...tab }
|
||||
|
||||
@@ -400,6 +408,46 @@ export const useWorktabStore = defineStore(
|
||||
keepAliveExclude.value = keepAliveExclude.value.filter((item) => item !== name)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭最早打开的可关闭标签(数组中最靠前的非 fixedTab)
|
||||
*/
|
||||
const evictOldestClosableTab = (): boolean => {
|
||||
const index = opened.value.findIndex(isTabClosable)
|
||||
if (index === -1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const [removed] = opened.value.splice(index, 1)
|
||||
if (removed?.name) {
|
||||
addKeepAliveExclude(removed)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 新开标签前:保证 opened.length < MAX_OPEN_TABS
|
||||
*/
|
||||
const ensureCapacityForNewTab = (): boolean => {
|
||||
while (opened.value.length >= MAX_OPEN_TABS) {
|
||||
if (!evictOldestClosableTab()) {
|
||||
console.warn(`工作标签已达上限 ${MAX_OPEN_TABS} 个,且均为固定标签,无法打开新页面`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 持久化恢复或校验后:将数量压回上限以内
|
||||
*/
|
||||
const trimTabsToMaxOpen = (): void => {
|
||||
while (opened.value.length > MAX_OPEN_TABS) {
|
||||
if (!evictOldestClosableTab()) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将传入的一组选项卡的组件名称标记为排除缓存
|
||||
*/
|
||||
@@ -480,6 +528,8 @@ export const useWorktabStore = defineStore(
|
||||
opened.value = validTabs
|
||||
}
|
||||
|
||||
trimTabsToMaxOpen()
|
||||
|
||||
// 验证当前激活标签的有效性
|
||||
const isCurrentValid =
|
||||
current.value && !shouldIgnoreTab(current.value) && isTabRouteValid(current.value)
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
.upload-card {
|
||||
width: 140px;
|
||||
height: 96px;
|
||||
border: 1px dashed #a7f3d0;
|
||||
border-radius: 8px;
|
||||
background: #f0fdf4;
|
||||
border: 1px dashed var(--el-color-primary-light-7, #a7f3d0);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
background: var(--primary-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -22,15 +22,15 @@
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
border-color: var(--primary);
|
||||
border-style: solid;
|
||||
background: #ecfdf5;
|
||||
background: var(--primary-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-card-text {
|
||||
font-size: 12px;
|
||||
color: #10b981;
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -38,16 +38,16 @@
|
||||
.upload-hint {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #1e293b;
|
||||
color: var(--text-muted);
|
||||
margin-top: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
width: 140px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
background: var(--surface);
|
||||
overflow: hidden;
|
||||
|
||||
&.is-uploading {
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
background: var(--table-head);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
color: #1e293b;
|
||||
color: var(--text);
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -126,17 +126,17 @@
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
background: #f1f5f9;
|
||||
color: #1e293b;
|
||||
background: var(--row-hover);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #10b981;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&--danger:hover {
|
||||
background: #ef4444;
|
||||
background: var(--danger);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
+117
-1095
File diff suppressed because it is too large.
Load diff
@@ -32,7 +32,7 @@
|
||||
flex-shrink: 0;
|
||||
padding-right: 8px;
|
||||
margin-right: 4px;
|
||||
border-right: 1px solid var(--mint-border, #d9e8e2);
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.list-toolbar-date-shortcuts .date-shortcut-btn.el-button {
|
||||
@@ -42,14 +42,14 @@
|
||||
}
|
||||
|
||||
.list-toolbar-date-shortcuts .date-shortcut-btn.is-active.el-button {
|
||||
color: var(--mint-primary-dark, #059669) !important;
|
||||
background: var(--mint-primary-bg, #ecfdf5) !important;
|
||||
color: var(--primary-dark) !important;
|
||||
background: var(--primary-bg) !important;
|
||||
border-color: rgba(16, 185, 129, 0.35) !important;
|
||||
--el-button-text-color: var(--mint-primary-dark, #059669);
|
||||
--el-button-bg-color: var(--mint-primary-bg, #ecfdf5);
|
||||
--el-button-text-color: var(--primary-dark);
|
||||
--el-button-bg-color: var(--primary-bg);
|
||||
--el-button-border-color: rgba(16, 185, 129, 0.35);
|
||||
--el-button-hover-text-color: var(--mint-primary-dark, #059669);
|
||||
--el-button-hover-bg-color: var(--mint-primary-bg, #ecfdf5);
|
||||
--el-button-hover-text-color: var(--primary-dark);
|
||||
--el-button-hover-bg-color: var(--primary-bg);
|
||||
--el-button-hover-border-color: rgba(16, 185, 129, 0.45);
|
||||
}
|
||||
|
||||
@@ -138,21 +138,21 @@
|
||||
padding-left: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e293b);
|
||||
border-left: 3px solid var(--primary, #10b981);
|
||||
color: var(--text);
|
||||
border-left: 3px solid var(--primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.db-related-table-dialog .view-subtable-wrap {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md, 8px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.db-related-table-dialog .view-subtable-wrap .el-table.view-subtable {
|
||||
--el-table-header-bg-color: #f8fafc;
|
||||
--el-table-header-text-color: #1e293b;
|
||||
--el-table-border-color: #e2e8f0;
|
||||
--el-table-header-bg-color: var(--table-head);
|
||||
--el-table-header-text-color: var(--text);
|
||||
--el-table-border-color: var(--border);
|
||||
width: 100%;
|
||||
|
||||
.el-table__header th.el-table__cell,
|
||||
@@ -163,21 +163,21 @@
|
||||
|
||||
.el-table__header th.el-table__cell .cell {
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.el-table__body td.el-table__cell .cell {
|
||||
color: #1e293b;
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
/* 仪表盘图表卡片(避免嵌套 panel-top 与页面主标题重复) */
|
||||
.db-chart-card {
|
||||
height: 100%;
|
||||
background: var(--surface, #fff);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border-radius: var(--radius, 10px);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -221,8 +221,8 @@
|
||||
padding: 20px 12px;
|
||||
background: var(--primary-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border-radius: var(--radius, 10px);
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
.db-stat-title {
|
||||
margin: 0 0 10px;
|
||||
@@ -410,12 +410,12 @@
|
||||
}
|
||||
|
||||
.pound-slip-capture-inline .capture-row .label {
|
||||
color: #1e293b;
|
||||
color: var(--text);
|
||||
min-width: 82px;
|
||||
}
|
||||
|
||||
.pound-slip-capture-inline .capture-row .value {
|
||||
color: #111827;
|
||||
color: var(--text);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -481,7 +481,7 @@
|
||||
.pound-slip-capture-inline .capture-id {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
color: var(--text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -499,6 +499,6 @@
|
||||
.pound-slip-capture-inline .capture-desc {
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { fetchCategoryList } from '@/api/admin'
|
||||
import { fetchCategoryList } from '@/api/demo'
|
||||
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
|
||||
import { buildTreePathLabelFromFlat } from '@/utils/tree-path'
|
||||
|
||||
|
||||
@@ -25,6 +25,25 @@ export function pickBxData<T>(res: unknown): T {
|
||||
return res as T
|
||||
}
|
||||
|
||||
/** 解析分页列表响应 */
|
||||
export function pickBxPageResult<T>(res: unknown): { list: T[]; total: number } {
|
||||
const payload = pickBxData<{ list?: T[]; rows?: T[]; total?: number } | T[]>(res)
|
||||
if (Array.isArray(payload)) {
|
||||
return { list: payload, total: payload.length }
|
||||
}
|
||||
if (payload && typeof payload === 'object') {
|
||||
const record = payload as { list?: T[]; rows?: T[]; total?: number }
|
||||
const list = Array.isArray(record.list)
|
||||
? record.list
|
||||
: Array.isArray(record.rows)
|
||||
? record.rows
|
||||
: []
|
||||
const total = typeof record.total === 'number' ? record.total : list.length
|
||||
return { list, total }
|
||||
}
|
||||
return { list: [], total: 0 }
|
||||
}
|
||||
|
||||
/** 解析列表类响应,保证始终返回数组 */
|
||||
export function pickBxList<T>(res: unknown): T[] {
|
||||
const payload = pickBxData<T[] | { list?: T[]; rows?: T[] }>(res)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { CertRead, VehicleDetail } from '@/api/pl'
|
||||
import type { VehiclePersonnelViewRow } from '@/components/business/pl-vehicle-personnel-view-list/index.vue'
|
||||
import type { VehicleViewBundle } from '@/composables/usePlVehicleViewDetail'
|
||||
|
||||
export interface TransportResourceSnapshotCert extends CertRead {}
|
||||
|
||||
export interface TransportResourceSnapshotPersonnel {
|
||||
personnelId?: string
|
||||
personnelName: string
|
||||
phoneNo?: string
|
||||
age?: number | null
|
||||
idCardNo?: string
|
||||
role: string
|
||||
isDefault?: number
|
||||
remark?: string
|
||||
certList?: TransportResourceSnapshotCert[]
|
||||
}
|
||||
|
||||
export interface TransportResourceSnapshot {
|
||||
snapshottedAt?: string
|
||||
vehicleId: string
|
||||
vehicleType: string
|
||||
carrierId?: string
|
||||
carrierName?: string
|
||||
licensePlate: string
|
||||
trailerNo?: string
|
||||
loadCapacity?: number | string | null
|
||||
vehicleStatus: string
|
||||
certList?: TransportResourceSnapshotCert[]
|
||||
personnelList?: TransportResourceSnapshotPersonnel[]
|
||||
personnelSummary?: string
|
||||
selectedDriverId?: string
|
||||
}
|
||||
|
||||
/** 将计划/运输单上的资源快照转为车辆查看组件可消费的 bundle */
|
||||
export function snapshotToVehicleViewBundle(
|
||||
snapshot: TransportResourceSnapshot
|
||||
): VehicleViewBundle {
|
||||
const detail: VehicleDetail = {
|
||||
id: snapshot.vehicleId,
|
||||
vehicleType: snapshot.vehicleType,
|
||||
carrierId: snapshot.carrierId,
|
||||
carrierName: snapshot.carrierName,
|
||||
licensePlate: snapshot.licensePlate,
|
||||
trailerNo: snapshot.trailerNo,
|
||||
loadCapacity: snapshot.loadCapacity != null ? Number(snapshot.loadCapacity) : undefined,
|
||||
vehicleStatus: snapshot.vehicleStatus,
|
||||
certList: (snapshot.certList || []) as CertRead[],
|
||||
createTime: snapshot.snapshottedAt || ''
|
||||
}
|
||||
|
||||
const personnelList: VehiclePersonnelViewRow[] = (snapshot.personnelList || []).map(person => ({
|
||||
personnelId: person.personnelId,
|
||||
personnelName: person.personnelName,
|
||||
phoneNo: person.phoneNo,
|
||||
age: person.age,
|
||||
role: person.role,
|
||||
isDefault: person.isDefault,
|
||||
remark: person.remark,
|
||||
certList: person.certList || []
|
||||
}))
|
||||
|
||||
return { detail, personnelList }
|
||||
}
|
||||
@@ -33,6 +33,10 @@
|
||||
* @module utils/table/tableConfig
|
||||
* @author Art Design Pro Team
|
||||
*/
|
||||
|
||||
/** 列表分页默认每页条数 */
|
||||
export const DEFAULT_LIST_PAGE_SIZE = 20
|
||||
|
||||
export const tableConfig = {
|
||||
// 响应数据字段映射配置,系统会从接口返回数据中按顺序查找这些字段
|
||||
// 列表数据
|
||||
@@ -51,5 +55,8 @@ export const tableConfig = {
|
||||
current: 'current',
|
||||
// 每页大小
|
||||
size: 'size'
|
||||
}
|
||||
},
|
||||
|
||||
/** 列表分页默认每页条数 */
|
||||
defaultPageSize: DEFAULT_LIST_PAGE_SIZE
|
||||
}
|
||||
@@ -91,15 +91,34 @@
|
||||
<el-button class="btn-main" type="primary" @click="openAdd">
|
||||
<el-icon><Plus /></el-icon>新增备用金申请
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handleToolbarEdit">
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1 || !isDraftEditable(selectedRows[0])"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="
|
||||
selectedRows.length === 0 ||
|
||||
!selectedRows.every(isDraftEditable) ||
|
||||
!allRowsDeletableByCreator(selectedRows)
|
||||
"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleToolbarDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="loadingMap.submit" @click="handleToolbarSubmit">
|
||||
<el-icon><Upload /></el-icon>提交审批
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openFlowDialog()">
|
||||
<el-icon><View /></el-icon>查看审批流
|
||||
</el-button>
|
||||
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -114,6 +133,7 @@
|
||||
:summary-method="summaryMethod"
|
||||
:max-height="'min(520px, calc(100vh - 300px))'"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
>
|
||||
<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" />
|
||||
@@ -132,7 +152,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="applicantName"
|
||||
label="申请人"
|
||||
@@ -202,38 +226,6 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="360" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap">
|
||||
<el-button class="btn-op-item" @click="openView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'draft' || row.status === 'rejected'"
|
||||
class="btn-op-item"
|
||||
@click="openEdit(row)"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'draft' || row.status === 'rejected'"
|
||||
class="btn-op-item"
|
||||
:loading="loadingMap[`submit_${row.id}`]"
|
||||
@click="handleSubmit(row)"
|
||||
>
|
||||
<el-icon><Upload /></el-icon>提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="(row.status === 'draft' || row.status === 'rejected') && canDeleteByCreator(row)"
|
||||
class="btn-op-item"
|
||||
:loading="loadingMap[`del_${row.id}`]"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -312,6 +304,27 @@
|
||||
<p class="dlg-view-meta">
|
||||
申请金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b>
|
||||
</p>
|
||||
<div v-if="isDraftEditable(viewData)" class="dlg-view-hero-actions">
|
||||
<el-button class="btn-main" type="primary" @click="openEdit(viewData)">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:loading="loadingMap[`submit_${viewData.id}`]"
|
||||
@click="handleViewSubmit"
|
||||
>
|
||||
<el-icon><Upload /></el-icon>提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteByCreator(viewData)"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:loading="loadingMap[`del_${viewData.id}`]"
|
||||
@click="handleViewDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dlg-view-section">
|
||||
@@ -416,6 +429,7 @@
|
||||
</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 { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
@@ -433,7 +447,7 @@ import {
|
||||
ArrowDownBold
|
||||
} from '@element-plus/icons-vue'
|
||||
import { advanceApi, paymentApi } from '@/api/bx'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { canDeleteByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
statusLabel,
|
||||
statusTagType,
|
||||
@@ -602,6 +616,11 @@ function handleReset() {
|
||||
search()
|
||||
}
|
||||
|
||||
function isDraftEditable(row?: any) {
|
||||
if (!row) return false
|
||||
return row.status === 'draft' || row.status === 'rejected'
|
||||
}
|
||||
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先勾选要编辑的申请')
|
||||
@@ -615,6 +634,55 @@ function handleToolbarEdit() {
|
||||
openEdit(row)
|
||||
}
|
||||
|
||||
async function handleToolbarDelete() {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先勾选要删除的申请')
|
||||
return
|
||||
}
|
||||
const targets = selectedRows.value.filter(isDraftEditable)
|
||||
if (targets.length !== selectedRows.value.length) {
|
||||
ElMessage.warning('仅草稿或已驳回的申请可删除')
|
||||
return
|
||||
}
|
||||
if (!allRowsDeletableByCreator(targets)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${targets.length} 条申请吗?删除后不可恢复。`, '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
for (const row of targets) {
|
||||
await advanceApi.delete(row.id)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
await refreshListAndStats()
|
||||
} catch (e: any) {
|
||||
if (e !== 'cancel') {
|
||||
ElMessage.error(e?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewSubmit() {
|
||||
if (!viewData.value?.id) return
|
||||
await handleSubmit(viewData.value)
|
||||
try {
|
||||
const res = await advanceApi.getById(viewData.value.id)
|
||||
viewData.value = res.data || res
|
||||
} catch {
|
||||
dialogs.view = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewDelete() {
|
||||
if (!viewData.value?.id) return
|
||||
await handleDelete(viewData.value)
|
||||
dialogs.view = false
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
Object.assign(addForm, { reason: '', amount: '', remark: '' })
|
||||
editingId.value = null
|
||||
|
||||
@@ -14,8 +14,11 @@
|
||||
<h2 class="title-bar-heading">备用金审批</h2>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tabs
|
||||
v-model="activeTab"
|
||||
class="panel-pill-tabs panel-pill-tabs--lg"
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<!-- 待我审批 -->
|
||||
<el-tab-pane label="待我审批" name="pending">
|
||||
<div class="filter-block">
|
||||
@@ -70,6 +73,8 @@
|
||||
<el-button class="btn-ghost" @click="handleBatchReject">
|
||||
<el-icon><Close /></el-icon>驳回
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions :loading="pendingLoading" @refresh="refreshPending" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,6 +89,7 @@
|
||||
:summary-method="pendingSummaryMethod"
|
||||
:max-height="'min(520px, calc(100vh - 300px))'"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
>
|
||||
<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" />
|
||||
@@ -102,7 +108,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<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
|
||||
prop="reason"
|
||||
@@ -133,21 +143,6 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap">
|
||||
<el-button class="btn-op-item" @click="openView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="openApproveDialog([row], 'approve')">
|
||||
<el-icon><Check /></el-icon>通过
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="openApproveDialog([row], 'reject')">
|
||||
<el-icon><Close /></el-icon>驳回
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -218,6 +213,7 @@
|
||||
show-summary
|
||||
:summary-method="approvedSummaryMethod"
|
||||
:max-height="'min(520px, calc(100vh - 300px))'"
|
||||
@row-dblclick="openView"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -235,7 +231,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<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
|
||||
prop="reason"
|
||||
@@ -273,13 +273,6 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="120" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="openView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -297,7 +290,6 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 详情 -->
|
||||
@@ -322,6 +314,14 @@
|
||||
<p class="dlg-view-meta">
|
||||
申请金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b>
|
||||
</p>
|
||||
<div v-if="activeTab === 'pending'" class="dlg-view-hero-actions">
|
||||
<el-button class="btn-main" type="primary" @click="openApproveDialog([viewData], 'approve')">
|
||||
<el-icon><Check /></el-icon>通过
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openApproveDialog([viewData], 'reject')">
|
||||
<el-icon><Close /></el-icon>驳回
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dlg-view-section">
|
||||
@@ -393,8 +393,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Check, Close, View, Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
import { Check, Close, Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
import { advanceApi } from '@/api/bx'
|
||||
import { advanceApproverStats, refreshApproveMenuBadges } from '@/hooks/bx/useApproveMenuBadge'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
|
||||
@@ -14,8 +14,11 @@
|
||||
<h2 class="title-bar-heading">备用金发放</h2>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tabs
|
||||
v-model="activeTab"
|
||||
class="panel-pill-tabs panel-pill-tabs--lg"
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<el-tab-pane label="待发放" name="pending">
|
||||
<div class="filter-block">
|
||||
<el-form :model="filterForm" class="filter-form" @submit.prevent>
|
||||
@@ -69,6 +72,8 @@
|
||||
<el-button class="btn-ghost" :loading="loadingMap.export" @click="handleExport">
|
||||
<el-icon><Download /></el-icon>导出
|
||||
</el-button>
|
||||
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +88,7 @@
|
||||
:summary-method="advanceIssueSummary"
|
||||
:max-height="'min(520px, calc(100vh - 360px))'"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
>
|
||||
<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" />
|
||||
@@ -101,7 +107,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<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
|
||||
prop="reason"
|
||||
@@ -154,18 +164,6 @@
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap">
|
||||
<el-button class="btn-op-item" @click="openIssueDialog(row)">
|
||||
<el-icon><Check /></el-icon>标记发放
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="openView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -230,6 +228,8 @@
|
||||
<el-button class="btn-ghost" :loading="loadingMap.export" @click="handleExport">
|
||||
<el-icon><Download /></el-icon>导出
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -243,6 +243,7 @@
|
||||
show-summary
|
||||
:summary-method="advanceIssueSummary"
|
||||
:max-height="'min(520px, calc(100vh - 360px))'"
|
||||
@row-dblclick="openView"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -260,7 +261,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<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
|
||||
prop="reason"
|
||||
@@ -307,13 +312,6 @@
|
||||
</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 label="操作" min-width="120" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="openView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -331,7 +329,6 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 标记发放 -->
|
||||
@@ -404,6 +401,11 @@
|
||||
实际发放金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.actualAmount) }}</b>
|
||||
</template>
|
||||
</p>
|
||||
<div v-if="activeTab === 'pending'" class="dlg-view-hero-actions">
|
||||
<el-button class="btn-main" type="primary" @click="openIssueDialog(viewData)">
|
||||
<el-icon><Check /></el-icon>标记发放
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dlg-view-section">
|
||||
@@ -460,9 +462,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Check, Download, View, Close, Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
import { Check, Download, Close, Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
import { paymentApi, advanceApi } from '@/api/bx'
|
||||
import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
|
||||
import { formatSumMoney, formatMoney, formatMoneyYuan, moneyTableFormatter } from '@/utils/bx/format'
|
||||
|
||||
@@ -46,17 +46,45 @@
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">0</span> 项
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="openAddDialog">
|
||||
<el-icon><Plus /></el-icon>新增一级项目
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
:loading="loadingMap.rowEdit"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedRows.length === 1 && selectedRows[0]?.level === 1"
|
||||
class="btn-ghost"
|
||||
:loading="loadingMap.rowAddChild"
|
||||
@click="handleToolbarAddChild"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>新增子项
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
|
||||
:title="selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0]) ? CREATOR_DELETE_TIP : ''"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="handleToolbarDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="expandAll">
|
||||
<el-icon><SortDown /></el-icon>展开全部
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="collapseAll">
|
||||
<el-icon><SortUp /></el-icon>收起全部
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +97,9 @@
|
||||
stripe
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'childCount > 0' }"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<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
|
||||
prop="itemName"
|
||||
@@ -110,32 +140,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap">
|
||||
<el-button class="btn-op-item" :loading="loadingMap.rowEdit" @click="openEditDialog(row)">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.level === 1"
|
||||
class="btn-op-item"
|
||||
:loading="loadingMap.rowAddChild"
|
||||
@click="openAddChildDialog(row)"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>新增子项
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
@@ -316,6 +320,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { reactive, ref, onMounted, computed, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
@@ -353,6 +358,7 @@ const searchForm = reactive({
|
||||
|
||||
const expenseTree = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
|
||||
const formData = reactive({
|
||||
id: 0 as number,
|
||||
@@ -447,6 +453,34 @@ function openAddDialog() {
|
||||
dialogs.addProject = true
|
||||
}
|
||||
|
||||
function onSelectionChange(rows: any[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请先勾选一条费用项目')
|
||||
return
|
||||
}
|
||||
openEditDialog(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarAddChild() {
|
||||
if (selectedRows.value.length !== 1 || selectedRows.value[0]?.level !== 1) {
|
||||
ElMessage.warning('请先勾选一条一级项目')
|
||||
return
|
||||
}
|
||||
openAddChildDialog(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarDelete() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请先勾选一条费用项目')
|
||||
return
|
||||
}
|
||||
handleDelete(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function openEditDialog(row: any) {
|
||||
Object.assign(formData, {
|
||||
id: row.id,
|
||||
|
||||
@@ -14,8 +14,11 @@
|
||||
<h2 class="title-bar-heading">支付管理</h2>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tabs
|
||||
v-model="activeTab"
|
||||
class="panel-pill-tabs panel-pill-tabs--lg"
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<!-- 待处理 -->
|
||||
<el-tab-pane label="待处理" name="pending">
|
||||
<div class="filter-block">
|
||||
@@ -70,6 +73,8 @@
|
||||
<el-button class="btn-ghost" @click="handleToolbarOffset">
|
||||
<el-icon><Switch /></el-icon>冲抵备用金
|
||||
</el-button>
|
||||
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -84,6 +89,7 @@
|
||||
:summary-method="summaryMethod"
|
||||
:max-height="'min(520px, calc(100vh - 360px))'"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
>
|
||||
<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" />
|
||||
@@ -102,7 +108,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<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
|
||||
prop="title"
|
||||
@@ -135,21 +145,6 @@
|
||||
}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap">
|
||||
<el-button class="btn-op-item" @click="openView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="openIssueDialog(row)">
|
||||
<el-icon><Money /></el-icon>发放
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="openOffsetDialog(row)">
|
||||
<el-icon><Switch /></el-icon>冲抵
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -230,6 +225,7 @@
|
||||
show-summary
|
||||
:summary-method="processedSummaryMethod"
|
||||
:max-height="'min(520px, calc(100vh - 300px))'"
|
||||
@row-dblclick="openProcessedView"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -247,7 +243,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<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
|
||||
prop="title"
|
||||
@@ -292,13 +292,6 @@
|
||||
{{ row.payTime || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="120" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="openProcessedView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -316,7 +309,6 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 单据详情 -->
|
||||
@@ -341,6 +333,14 @@
|
||||
<p class="dlg-view-meta">
|
||||
金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b>
|
||||
</p>
|
||||
<div v-if="activeTab === 'pending'" class="dlg-view-hero-actions">
|
||||
<el-button class="btn-main" type="primary" @click="openIssueDialog(viewData)">
|
||||
<el-icon><Money /></el-icon>发放
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openOffsetDialog(viewData)">
|
||||
<el-icon><Switch /></el-icon>冲抵备用金
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">基础信息</h4>
|
||||
@@ -599,6 +599,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
@@ -1174,18 +1175,3 @@ function processedStatusTagType(
|
||||
return 'info'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text, #1e293b);
|
||||
}
|
||||
|
||||
.offset-pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -63,12 +63,34 @@
|
||||
<el-button class="btn-main" type="primary" @click="openAddDialog">
|
||||
<el-icon><Plus /></el-icon>新增报销申请
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1 || !isDraftEditable(selectedRows[0])"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="
|
||||
selectedRows.length === 0 ||
|
||||
!selectedRows.every(isDraftEditable) ||
|
||||
!allRowsDeletableByCreator(selectedRows)
|
||||
"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleToolbarDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="loadingMap.submit" @click="handleToolbarSubmit">
|
||||
<el-icon><Upload /></el-icon>提交审批
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openFlowDialog()">
|
||||
<el-icon><View /></el-icon>查看审批流
|
||||
</el-button>
|
||||
<ListToolbarEndActions :loading="loading" @refresh="handleSearch" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -83,6 +105,7 @@
|
||||
:summary-method="summaryMethod"
|
||||
:max-height="'min(520px, calc(100vh - 300px))'"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
>
|
||||
<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" />
|
||||
@@ -101,7 +124,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openView(row)">{{ row.applyCode }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="title"
|
||||
label="报销标题"
|
||||
@@ -146,38 +173,6 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="360" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap">
|
||||
<el-button class="btn-op-item" @click="openView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'draft' || row.status === 'rejected'"
|
||||
class="btn-op-item"
|
||||
@click="openEditDialog(row)"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'draft' || row.status === 'rejected'"
|
||||
class="btn-op-item"
|
||||
:loading="loadingMap[`submit_${row.id}`]"
|
||||
@click="handleSubmit(row)"
|
||||
>
|
||||
<el-icon><Upload /></el-icon>提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="(row.status === 'draft' || row.status === 'rejected') && canDeleteByCreator(row)"
|
||||
class="btn-op-item"
|
||||
:loading="loadingMap[`del_${row.id}`]"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -342,6 +337,27 @@
|
||||
<p class="dlg-view-meta">
|
||||
报销金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b>
|
||||
</p>
|
||||
<div v-if="isDraftEditable(viewData)" class="dlg-view-hero-actions">
|
||||
<el-button class="btn-main" type="primary" @click="openEditDialog(viewData)">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:loading="loadingMap[`submit_${viewData.id}`]"
|
||||
@click="handleViewSubmit"
|
||||
>
|
||||
<el-icon><Upload /></el-icon>提交
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canDeleteByCreator(viewData)"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:loading="loadingMap[`del_${viewData.id}`]"
|
||||
@click="handleViewDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dlg-view-section">
|
||||
@@ -459,6 +475,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
@@ -474,7 +491,7 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { reimburseApi, attachmentApi } from '@/api/bx'
|
||||
import { type BxAttachmentItem, loadBxAttachments } from '@/utils/bx/attachment'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { canDeleteByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { useExpenseItemTree, type ExpenseItem } from '@/hooks/bx/useExpenseItemTree'
|
||||
import {
|
||||
statusLabel,
|
||||
@@ -676,6 +693,68 @@ async function openAddDialog() {
|
||||
await refreshGeneratedTitle()
|
||||
}
|
||||
|
||||
function isDraftEditable(row?: any) {
|
||||
if (!row) return false
|
||||
return row.status === 'draft' || row.status === 'rejected'
|
||||
}
|
||||
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请先勾选一条报销单')
|
||||
return
|
||||
}
|
||||
openEditDialog(selectedRows.value[0])
|
||||
}
|
||||
|
||||
async function handleToolbarDelete() {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先勾选要删除的报销单')
|
||||
return
|
||||
}
|
||||
const targets = selectedRows.value.filter(isDraftEditable)
|
||||
if (targets.length !== selectedRows.value.length) {
|
||||
ElMessage.warning('仅草稿或已驳回的报销单可删除')
|
||||
return
|
||||
}
|
||||
if (!allRowsDeletableByCreator(targets)) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${targets.length} 条报销单吗?删除后不可恢复。`, '删除确认', {
|
||||
confirmButtonText: '确定删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
for (const row of targets) {
|
||||
await reimburseApi.delete(row.id)
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
await refreshListAndStats()
|
||||
} catch (e: any) {
|
||||
if (e !== 'cancel') {
|
||||
ElMessage.error(e?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewSubmit() {
|
||||
if (!viewData.value?.id) return
|
||||
await handleSubmit(viewData.value)
|
||||
try {
|
||||
const res = await reimburseApi.getById(viewData.value.id)
|
||||
viewData.value = res.data || res
|
||||
} catch {
|
||||
dialogs.view = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewDelete() {
|
||||
if (!viewData.value?.id) return
|
||||
await handleDelete(viewData.value)
|
||||
dialogs.view = false
|
||||
}
|
||||
|
||||
async function openEditDialog(row: any) {
|
||||
try {
|
||||
const res: any = await reimburseApi.getById(row.id)
|
||||
@@ -960,83 +1039,3 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
return sums
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.detail-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-table th {
|
||||
background: var(--mint-table-head, #f8fafc);
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--mint-border, #d9e8e2);
|
||||
color: var(--mint-text);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-table td {
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid var(--mint-border, #d9e8e2);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.detail-table .col-index {
|
||||
width: 56px;
|
||||
text-align: center;
|
||||
color: var(--mint-text);
|
||||
}
|
||||
|
||||
.detail-table .col-expense {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.detail-table .col-amount {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.detail-table .col-remark {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.th-required {
|
||||
color: var(--el-color-danger);
|
||||
margin-left: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-table .col-action {
|
||||
width: 72px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.detail-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.detail-total {
|
||||
font-size: 14px;
|
||||
color: var(--mint-text, #1e293b);
|
||||
}
|
||||
|
||||
.detail-total b {
|
||||
color: var(--danger, #dc2626);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.view-detail-table :deep(.el-table__footer-wrapper) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -14,8 +14,11 @@
|
||||
<h2 class="title-bar-heading">报销审批</h2>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs">
|
||||
<el-tabs v-model="activeTab" @tab-change="handleTabChange">
|
||||
<el-tabs
|
||||
v-model="activeTab"
|
||||
class="panel-pill-tabs panel-pill-tabs--lg"
|
||||
@tab-change="handleTabChange"
|
||||
>
|
||||
<!-- 待我审批 -->
|
||||
<el-tab-pane label="待我审批" name="pending">
|
||||
<div class="filter-block">
|
||||
@@ -119,6 +122,8 @@
|
||||
<el-button class="btn-ghost" @click="handleBatchReject">
|
||||
<el-icon><Close /></el-icon>驳回
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions :loading="pendingLoading" @refresh="refreshPending" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -133,6 +138,7 @@
|
||||
:summary-method="pendingSummaryMethod"
|
||||
:max-height="'min(520px, calc(100vh - 300px))'"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openView"
|
||||
>
|
||||
<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" />
|
||||
@@ -151,7 +157,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<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
|
||||
prop="title"
|
||||
@@ -190,21 +200,6 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap">
|
||||
<el-button class="btn-op-item" @click="openView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="openApproveDialog([row], 'approve')">
|
||||
<el-icon><Check /></el-icon>通过
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="openApproveDialog([row], 'reject')">
|
||||
<el-icon><Close /></el-icon>驳回
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -324,6 +319,7 @@
|
||||
show-summary
|
||||
:summary-method="approvedSummaryMethod"
|
||||
:max-height="'min(520px, calc(100vh - 300px))'"
|
||||
@row-dblclick="openView"
|
||||
>
|
||||
<el-table-column type="index" label="序号" min-width="68" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -341,7 +337,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<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
|
||||
prop="title"
|
||||
@@ -387,13 +387,6 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="120" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="openView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -411,7 +404,6 @@
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 详情 -->
|
||||
@@ -436,6 +428,14 @@
|
||||
<p class="dlg-view-meta">
|
||||
金额(元):<b class="money-em">{{ formatMoneyYuan(viewData.amount || 0) }}</b>
|
||||
</p>
|
||||
<div v-if="activeTab === 'pending'" class="dlg-view-hero-actions">
|
||||
<el-button class="btn-main" type="primary" @click="openApproveDialog([viewData], 'approve')">
|
||||
<el-icon><Check /></el-icon>通过
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openApproveDialog([viewData], 'reject')">
|
||||
<el-icon><Close /></el-icon>驳回
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dlg-view-section">
|
||||
@@ -537,6 +537,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
View,
|
||||
@@ -855,9 +856,3 @@ function viewDetailSummary({ columns, data }: { columns: any[]; data: any[] }) {
|
||||
return sums
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.view-detail-table :deep(.el-table__footer-wrapper) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -91,6 +91,20 @@
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<ListToolbarEndActions
|
||||
:loading="loading"
|
||||
:column-defs="listColumnDefs"
|
||||
v-model:visible-columns="visibleColumns"
|
||||
v-model:check-all="checkAll"
|
||||
:is-indeterminate="isIndeterminate"
|
||||
@refresh="loadData"
|
||||
@reset-columns="resetToDefault"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll table-page-summary">
|
||||
<el-table
|
||||
@@ -104,6 +118,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"
|
||||
@@ -118,16 +133,35 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="bizType" label="业务类型" min-width="100" align="center" header-align="center">
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openViewDetail(row)">{{ row.applyCode }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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"
|
||||
@@ -136,6 +170,7 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('amount')"
|
||||
prop="amount"
|
||||
label="申请金额(元)"
|
||||
:formatter="moneyTableFormatter"
|
||||
@@ -144,14 +179,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)"
|
||||
@@ -163,13 +212,6 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="120" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="openViewDetail(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -241,9 +283,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { View, Close, Search, RefreshLeft, ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
|
||||
import { Close, Search, RefreshLeft, ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
|
||||
import { advanceApi, reimburseApi } from '@/api/bx'
|
||||
import {
|
||||
bizTypeTagType,
|
||||
@@ -276,6 +320,24 @@ const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
const mergedRows = ref<any[]>([])
|
||||
const { applicantOptions, loadApplicantOptions } = useApplicantOptions()
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'applyTime', label: '申请时间' },
|
||||
{ prop: 'bizType', label: '业务类型' },
|
||||
{ prop: 'applicantName', label: '申请人' },
|
||||
{ prop: 'title', label: '申请标题' },
|
||||
{ prop: 'amount', label: '申请金额(元)' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
|
||||
]
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-view-all-list-columns', listColumnDefs)
|
||||
|
||||
const filterForm = reactive({
|
||||
code: '',
|
||||
bizType: '' as string,
|
||||
@@ -333,7 +395,7 @@ function filterMergedRows(rows: any[]) {
|
||||
|
||||
const filteredMergedRows = computed(() => filterMergedRows(mergedRows.value))
|
||||
|
||||
const pager = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 10 })
|
||||
const pager = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 20 })
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
const viewDetailKvFields = computed<ViewField[]>(() => {
|
||||
@@ -446,9 +508,18 @@ function handleReset() {
|
||||
pager.currentPage.value = 1
|
||||
}
|
||||
|
||||
function summaryMethod(params: { data: Array<{ amount?: number }> }) {
|
||||
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)
|
||||
return ['', `本页合计(${params.data.length}单)`, '', '', '', '', formatSumMoney(totalAmount), '', '', '']
|
||||
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 ''
|
||||
})
|
||||
}
|
||||
|
||||
async function openViewDetail(row: any) {
|
||||
|
||||
@@ -83,6 +83,20 @@
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<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>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<div class="table-scroll">
|
||||
<el-table
|
||||
@@ -94,6 +108,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"
|
||||
@@ -108,8 +123,19 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="bizType" label="业务类型" min-width="100" align="center" header-align="center">
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="openViewDetail(row)">{{ row.applyCode }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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 }}
|
||||
@@ -117,6 +143,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('title')"
|
||||
prop="title"
|
||||
label="申请标题"
|
||||
min-width="180"
|
||||
@@ -125,6 +152,7 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('amount')"
|
||||
prop="amount"
|
||||
label="申请金额(元)"
|
||||
:formatter="moneyTableFormatter"
|
||||
@@ -133,14 +161,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)"
|
||||
@@ -152,13 +194,6 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="120" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item" @click="openViewDetail(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -229,9 +264,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { computed, onActivated, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { View, Close, Search, RefreshLeft, ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
|
||||
import { Close, Search, RefreshLeft, ArrowUpBold, ArrowDownBold } from '@element-plus/icons-vue'
|
||||
import { advanceApi, reimburseApi } from '@/api/bx'
|
||||
import {
|
||||
bizTypeTagType,
|
||||
@@ -262,6 +299,23 @@ const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
|
||||
const mergedRows = ref<any[]>([])
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'applyTime', label: '申请时间' },
|
||||
{ prop: 'bizType', label: '业务类型' },
|
||||
{ prop: 'title', label: '申请标题' },
|
||||
{ prop: 'amount', label: '申请金额(元)' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'currentNode', label: '当前节点', defaultVisible: false }
|
||||
]
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('bx-view-mine-list-columns', listColumnDefs)
|
||||
|
||||
const filterForm = reactive({
|
||||
code: '',
|
||||
bizType: '' as string,
|
||||
@@ -310,7 +364,7 @@ const {
|
||||
pageSize,
|
||||
rangeText,
|
||||
onSizeChange
|
||||
} = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 10 })
|
||||
} = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 20 })
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
const viewDetailKvFields = computed<ViewField[]>(() => {
|
||||
|
||||
@@ -93,6 +93,8 @@
|
||||
<el-button class="btn-ghost" :loading="loadingMap.batchReject" @click="handleBatchReject">
|
||||
<el-icon><Close /></el-icon>批量驳回
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions :loading="tableLoading" @refresh="loadData" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,6 +109,7 @@
|
||||
:summary-method="summaryMethod"
|
||||
:max-height="'min(520px, calc(100vh - 300px))'"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openViewDetailFromRow"
|
||||
>
|
||||
<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" />
|
||||
@@ -125,7 +128,11 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<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">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="bizTypeTagType(row.bizType)" size="small">
|
||||
@@ -163,25 +170,6 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="280" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap">
|
||||
<el-button class="btn-op-item" @click="openViewDetail(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:loading="loadingMap[`pass_${row.id}`]"
|
||||
@click="handleApprove(row)"
|
||||
>
|
||||
<el-icon><Check /></el-icon>通过
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="openRejectDialog(row)">
|
||||
<el-icon><Close /></el-icon>驳回
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -228,6 +216,19 @@
|
||||
金额(元):<b class="money-em">{{ formatMoneyYuan(currentRow?.amount || 0) }}</b>
|
||||
· 申请人:{{ currentRow?.applicantName || '-' }}
|
||||
</p>
|
||||
<div v-if="currentRow" class="dlg-view-hero-actions">
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="loadingMap[`pass_${currentRow.id}`]"
|
||||
@click="handleApprove(currentRow)"
|
||||
>
|
||||
<el-icon><Check /></el-icon>通过
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="openRejectDialog(currentRow)">
|
||||
<el-icon><Close /></el-icon>驳回
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dlg-view-section">
|
||||
<h4 class="dlg-view-section-title">基础信息</h4>
|
||||
@@ -284,10 +285,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { computed, onActivated, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
View,
|
||||
Check,
|
||||
Close,
|
||||
Search,
|
||||
@@ -376,7 +377,7 @@ const {
|
||||
pageSize,
|
||||
rangeText,
|
||||
onSizeChange
|
||||
} = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 10 })
|
||||
} = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 20 })
|
||||
const currentRow = ref<any>(null)
|
||||
|
||||
const viewDetailKvFields = computed<ViewField[]>(() => {
|
||||
@@ -602,6 +603,10 @@ function summaryMethod(params: { data: Array<{ amount?: number }> }) {
|
||||
return ['', '', `本页合计(${params.data.length}单)`, '', '', '', '', formatSumMoney(totalAmount), '', '']
|
||||
}
|
||||
|
||||
function openViewDetailFromRow(row: any) {
|
||||
openViewDetail(row)
|
||||
}
|
||||
|
||||
async function openViewDetail(row: any) {
|
||||
currentRow.value = row
|
||||
viewFlowNodes.value = []
|
||||
|
||||
@@ -20,19 +20,51 @@
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">0</span> 项
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="openAddDialog">
|
||||
<el-icon><Plus /></el-icon>新增节点
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
|
||||
:title="selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0]) ? CREATOR_DELETE_TIP : ''"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="handleToolbarDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="handleToolbarMoveUp">
|
||||
<el-icon><ArrowUp /></el-icon>上移
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="handleToolbarMoveDown">
|
||||
<el-icon><ArrowDown /></el-icon>下移
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="loadingMap.save" @click="saveNodes">
|
||||
<el-icon><Upload /></el-icon>保存配置
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions :loading="loading" @refresh="loadWfDef" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="nodes" v-loading="loading" stripe>
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="nodes"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<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
|
||||
prop="nodeName"
|
||||
@@ -58,30 +90,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="360" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap">
|
||||
<el-button class="btn-op-item" @click="openEditDialog(row)">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="deleteNode(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="moveUp(row)">
|
||||
<el-icon><ArrowUp /></el-icon>上移
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="moveDown(row)">
|
||||
<el-icon><ArrowDown /></el-icon>下移
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
@@ -186,6 +194,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
@@ -212,6 +221,7 @@ const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<Record<string, any>[]>([])
|
||||
const wfDef = ref<Record<string, any>>({})
|
||||
const nodes = computed(() => wfDef.value.nodes || [])
|
||||
const userOptions = ref<
|
||||
@@ -269,6 +279,36 @@ onMounted(() => {
|
||||
loadUsers()
|
||||
})
|
||||
|
||||
function onSelectionChange(rows: Record<string, any>[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请先勾选一条节点')
|
||||
return
|
||||
}
|
||||
openEditDialog(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarDelete() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请先勾选一条节点')
|
||||
return
|
||||
}
|
||||
deleteNode(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarMoveUp() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
moveUp(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarMoveDown() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
moveDown(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function buildNodePayload(list: Record<string, any>[]) {
|
||||
return list.map((n, i) => ({
|
||||
nodeName: n.nodeName,
|
||||
|
||||
@@ -20,19 +20,51 @@
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">0</span> 项
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button class="btn-main" type="primary" @click="openAddDialog">
|
||||
<el-icon><Plus /></el-icon>新增节点
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
|
||||
:title="selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0]) ? CREATOR_DELETE_TIP : ''"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="handleToolbarDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="handleToolbarMoveUp">
|
||||
<el-icon><ArrowUp /></el-icon>上移
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="handleToolbarMoveDown">
|
||||
<el-icon><ArrowDown /></el-icon>下移
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="loadingMap.save" @click="saveNodes">
|
||||
<el-icon><Upload /></el-icon>保存配置
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions :loading="loading" @refresh="loadWfDef" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="nodes" v-loading="loading" stripe>
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="nodes"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<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
|
||||
prop="nodeName"
|
||||
@@ -58,30 +90,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="360" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px; flex-wrap: nowrap">
|
||||
<el-button class="btn-op-item" @click="openEditDialog(row)">
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
:loading="loadingMap.rowDelete"
|
||||
@click="deleteNode(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="moveUp(row)">
|
||||
<el-icon><ArrowUp /></el-icon>上移
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="moveDown(row)">
|
||||
<el-icon><ArrowDown /></el-icon>下移
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</section>
|
||||
@@ -186,6 +194,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, computed, onMounted } from 'vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
@@ -212,6 +221,7 @@ const dialogs = reactive<Record<DialogKey, boolean>>({
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<Record<string, any>[]>([])
|
||||
const wfDef = ref<Record<string, any>>({})
|
||||
const nodes = computed(() => wfDef.value.nodes || [])
|
||||
const userOptions = ref<
|
||||
@@ -269,6 +279,36 @@ onMounted(() => {
|
||||
loadUsers()
|
||||
})
|
||||
|
||||
function onSelectionChange(rows: Record<string, any>[]) {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请先勾选一条节点')
|
||||
return
|
||||
}
|
||||
openEditDialog(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarDelete() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请先勾选一条节点')
|
||||
return
|
||||
}
|
||||
deleteNode(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarMoveUp() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
moveUp(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarMoveDown() {
|
||||
if (selectedRows.value.length !== 1) return
|
||||
moveDown(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function buildNodePayload(list: Record<string, any>[]) {
|
||||
return list.map((n, i) => ({
|
||||
nodeName: n.nodeName,
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
const followQuery = ref<ClueFollowQuery>({
|
||||
clueId: '',
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
const clueFormRef = ref<InstanceType<typeof ClueForm>>()
|
||||
|
||||
@@ -3,18 +3,20 @@
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2>线索管理</h2>
|
||||
<h2 class="title-bar-heading">线索管理</h2>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs">
|
||||
<el-tabs v-model="activeView" @tab-change="handleViewChange">
|
||||
<el-tab-pane label="我的线索" name="mine" />
|
||||
<el-tab-pane label="全部线索" name="all" />
|
||||
<el-tab-pane label="待跟进" name="toFollow" />
|
||||
<el-tab-pane label="今日新增" name="today" />
|
||||
<el-tab-pane label="已转化" name="converted" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-tabs
|
||||
v-model="activeView"
|
||||
class="panel-pill-tabs panel-pill-tabs--lg"
|
||||
@tab-change="handleViewChange"
|
||||
>
|
||||
<el-tab-pane label="我的线索" name="mine" />
|
||||
<el-tab-pane label="全部线索" name="all" />
|
||||
<el-tab-pane label="待跟进" name="toFollow" />
|
||||
<el-tab-pane label="今日新增" name="today" />
|
||||
<el-tab-pane label="已转化" name="converted" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
@@ -157,6 +159,40 @@
|
||||
<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"
|
||||
@click="handleEdit()"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasCluePerm('follow')"
|
||||
class="btn-ghost"
|
||||
:disabled="
|
||||
single ||
|
||||
selectedRows[0]?.isConverted === 1 ||
|
||||
selectedRows[0]?.status === 'invalid'
|
||||
"
|
||||
@click="handleAddFollow()"
|
||||
>
|
||||
<el-icon><ChatLineRound /></el-icon>跟进
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasCluePerm('remove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="
|
||||
multiple ||
|
||||
selectedRows[0]?.isConverted === 1 ||
|
||||
!allRowsDeletableByCreator(selectedRows)
|
||||
"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasCluePerm('assign')"
|
||||
class="btn-ghost"
|
||||
@@ -181,6 +217,16 @@
|
||||
>
|
||||
<el-icon><CircleClose /></el-icon>标为无效
|
||||
</el-button>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -367,54 +413,6 @@
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="400"
|
||||
align="center"
|
||||
header-align="center"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button v-if="hasCluePerm('query')" class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasCluePerm('edit')"
|
||||
class="btn-op-item primary"
|
||||
:disabled="row.isConverted === 1"
|
||||
@click="handleEdit(row)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasCluePerm('follow')"
|
||||
class="btn-op-item primary"
|
||||
:disabled="row.isConverted === 1 || row.status === 'invalid'"
|
||||
@click="handleAddFollow(row)"
|
||||
>
|
||||
<el-icon><ChatLineRound /></el-icon>跟进
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasCluePerm('invalid')"
|
||||
class="btn-op-item op-warn"
|
||||
:disabled="row.isConverted === 1 || row.status === 'invalid'"
|
||||
@click="handleInvalid(row)"
|
||||
>
|
||||
<el-icon><CircleClose /></el-icon>无效
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasCluePerm('remove')"
|
||||
class="btn-op-item danger"
|
||||
:disabled="row.isConverted === 1 || !canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -438,45 +436,6 @@
|
||||
<InvalidForm ref="invalidFormRef" @success="getList" />
|
||||
<DetailDrawer ref="detailDrawerRef" @success="getList" />
|
||||
|
||||
<el-dialog
|
||||
v-model="columnSettingDialog.visible"
|
||||
title="列设置"
|
||||
width="600px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="column-setting-content">
|
||||
<el-checkbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAllChange"
|
||||
>
|
||||
全选
|
||||
</el-checkbox>
|
||||
<el-divider />
|
||||
<el-checkbox-group v-model="visibleColumns" @change="handleCheckedColumnsChange">
|
||||
<div v-for="col in tableColumns" :key="col.prop" class="column-item">
|
||||
<el-checkbox :label="col.prop" :disabled="col.fixed">{{ col.label }}</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button class="btn-ghost" @click="columnSettingDialog.visible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handleResetColumns">
|
||||
<el-icon><RefreshLeft /></el-icon>恢复默认
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSaveColumnSetting">
|
||||
<el-icon><Check /></el-icon>保存
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<el-dialog
|
||||
v-model="importDialogVisible"
|
||||
@@ -536,7 +495,6 @@
|
||||
Check,
|
||||
Search,
|
||||
RefreshLeft,
|
||||
View,
|
||||
User,
|
||||
ArrowUpBold,
|
||||
ArrowDownBold,
|
||||
@@ -550,7 +508,7 @@
|
||||
import type { UploadFile } from 'element-plus'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useCluePerm } from '@/hooks/business/useCluePerm'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
listClue,
|
||||
listMyClue,
|
||||
@@ -567,6 +525,8 @@
|
||||
import AssignForm from './assignForm.vue'
|
||||
import InvalidForm from './invalidForm.vue'
|
||||
import DetailDrawer from './detailDrawer.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const { loadPerms, hasCluePerm } = useCluePerm()
|
||||
@@ -589,7 +549,7 @@
|
||||
const clueList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const single = ref(true)
|
||||
const multiple = ref(true)
|
||||
@@ -602,38 +562,44 @@
|
||||
const invalidFormRef = ref<InstanceType<typeof InvalidForm>>()
|
||||
const detailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
|
||||
|
||||
const COLUMN_STORAGE_KEY = 'crm_clue_visible_columns'
|
||||
const tableColumns = ref([
|
||||
{ prop: 'company', label: '公司名称', fixed: false },
|
||||
{ prop: 'customerType', label: '客户类型', fixed: false },
|
||||
{ prop: 'mobile', label: '手机号', fixed: false },
|
||||
{ prop: 'source', label: '线索来源', fixed: false },
|
||||
{ prop: 'level', label: '线索等级', fixed: false },
|
||||
{ prop: 'status', label: '线索状态', fixed: false },
|
||||
{ prop: 'industry', label: '所属行业', fixed: false },
|
||||
{ prop: 'parkArea', label: '所在园区', fixed: false },
|
||||
{ prop: 'ownerUserName', label: '负责人', fixed: false },
|
||||
{ prop: 'followCount', label: '跟进次数', fixed: false },
|
||||
{ prop: 'lastTime', label: '最后跟进时间', fixed: false },
|
||||
{ prop: 'nextTime', label: '下次联系时间', fixed: false },
|
||||
{ prop: 'createTime', label: '创建时间', fixed: false }
|
||||
])
|
||||
const defaultVisibleColumns = [
|
||||
'company',
|
||||
'customerType',
|
||||
'mobile',
|
||||
'source',
|
||||
'level',
|
||||
'status',
|
||||
'parkArea',
|
||||
'ownerUserName',
|
||||
'followCount',
|
||||
'lastTime',
|
||||
'nextTime',
|
||||
'createTime'
|
||||
const listColumnDefs = [
|
||||
{ prop: 'company', label: '公司名称' },
|
||||
{ prop: 'customerType', label: '客户类型' },
|
||||
{ prop: 'mobile', label: '手机号' },
|
||||
{ prop: 'source', label: '线索来源' },
|
||||
{ prop: 'level', label: '线索等级' },
|
||||
{ prop: 'status', label: '线索状态' },
|
||||
{ prop: 'industry', label: '所属行业' },
|
||||
{ prop: 'parkArea', label: '所在园区' },
|
||||
{ prop: 'ownerUserName', label: '负责人' },
|
||||
{ prop: 'followCount', label: '跟进次数' },
|
||||
{ prop: 'lastTime', label: '最后跟进时间' },
|
||||
{ prop: 'nextTime', label: '下次联系时间' },
|
||||
{ prop: 'createTime', label: '创建时间' }
|
||||
]
|
||||
const visibleColumns = ref<string[]>([])
|
||||
const columnSettingDialog = reactive({ visible: false })
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('crm_clue_visible_columns', listColumnDefs, {
|
||||
defaultVisibleProps: [
|
||||
'company',
|
||||
'customerType',
|
||||
'mobile',
|
||||
'source',
|
||||
'level',
|
||||
'status',
|
||||
'parkArea',
|
||||
'ownerUserName',
|
||||
'followCount',
|
||||
'lastTime',
|
||||
'nextTime',
|
||||
'createTime'
|
||||
]
|
||||
})
|
||||
|
||||
const importDialogVisible = ref(false)
|
||||
const importFileList = ref<UploadFile[]>([])
|
||||
@@ -641,8 +607,6 @@
|
||||
const importUpdateSupport = ref(false)
|
||||
const templateLoading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const checkAll = ref(false)
|
||||
const isIndeterminate = ref(false)
|
||||
|
||||
const canConvertSelected = computed(() => {
|
||||
if (selectedRows.value.length !== 1) return false
|
||||
@@ -747,8 +711,13 @@
|
||||
clueFormRef.value?.open()
|
||||
}
|
||||
|
||||
function handleEdit(row: any) {
|
||||
clueFormRef.value?.open(row.id)
|
||||
function handleEdit(row?: any) {
|
||||
if (!row && ids.value.length !== 1) {
|
||||
ElMessage.warning('请选择一条数据进行编辑')
|
||||
return
|
||||
}
|
||||
const id = row?.id || ids.value[0]
|
||||
clueFormRef.value?.open(id)
|
||||
}
|
||||
|
||||
function handleView(row?: any) {
|
||||
@@ -857,55 +826,6 @@
|
||||
await getList()
|
||||
}
|
||||
|
||||
function initColumnSetting() {
|
||||
const saved = localStorage.getItem(COLUMN_STORAGE_KEY)
|
||||
if (saved) {
|
||||
try {
|
||||
visibleColumns.value = JSON.parse(saved)
|
||||
} catch {
|
||||
visibleColumns.value = [...defaultVisibleColumns]
|
||||
}
|
||||
} else {
|
||||
visibleColumns.value = [...defaultVisibleColumns]
|
||||
}
|
||||
updateCheckAllStatus()
|
||||
}
|
||||
|
||||
function isColumnVisible(prop: string) {
|
||||
return visibleColumns.value.includes(prop)
|
||||
}
|
||||
|
||||
function handleColumnSetting() {
|
||||
columnSettingDialog.visible = true
|
||||
}
|
||||
|
||||
function handleCheckAllChange(val: boolean) {
|
||||
visibleColumns.value = val ? tableColumns.value.map((col) => col.prop) : []
|
||||
isIndeterminate.value = false
|
||||
}
|
||||
|
||||
function handleCheckedColumnsChange() {
|
||||
updateCheckAllStatus()
|
||||
}
|
||||
|
||||
function updateCheckAllStatus() {
|
||||
const checkedCount = visibleColumns.value.length
|
||||
const totalCount = tableColumns.value.length
|
||||
checkAll.value = checkedCount === totalCount
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < totalCount
|
||||
}
|
||||
|
||||
function handleResetColumns() {
|
||||
visibleColumns.value = [...defaultVisibleColumns]
|
||||
updateCheckAllStatus()
|
||||
}
|
||||
|
||||
function handleSaveColumnSetting() {
|
||||
localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(visibleColumns.value))
|
||||
ElMessage.success('列设置保存成功')
|
||||
columnSettingDialog.visible = false
|
||||
}
|
||||
|
||||
function handleImport() {
|
||||
importFileList.value = []
|
||||
importUpdateSupport.value = false
|
||||
@@ -991,7 +911,6 @@
|
||||
|
||||
onMounted(() => {
|
||||
loadPerms()
|
||||
initColumnSetting()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
@@ -1005,13 +924,4 @@
|
||||
.quick-search-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.column-setting-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.column-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2>线索统计</h2>
|
||||
<h2 class="title-bar-heading">线索统计</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
|
||||
@@ -1,35 +1,46 @@
|
||||
<!-- 合同审批记录 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 查询表单 -->
|
||||
<el-card shadow="hover" v-show="showSearch" class="mb-[10px]">
|
||||
<el-form :model="queryParams" ref="queryFormRef" label-width="90px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">合同审批记录</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form ref="queryFormRef" :model="queryParams" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="合同编号" prop="contractNo">
|
||||
<el-input v-model="queryParams.contractNo" placeholder="请输入合同编号" clearable @keyup.enter="handleQuery" />
|
||||
<el-input
|
||||
v-model="queryParams.contractNo"
|
||||
placeholder="合同编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<el-form-item label="合同名称" prop="contractName">
|
||||
<el-input v-model="queryParams.contractName" placeholder="请输入合同名称" clearable @keyup.enter="handleQuery" />
|
||||
<el-input
|
||||
v-model="queryParams.contractName"
|
||||
placeholder="合同名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<el-form-item label="审批人" prop="approverName">
|
||||
<el-input v-model="queryParams.approverName" placeholder="请输入审批人" clearable @keyup.enter="handleQuery" />
|
||||
<el-input
|
||||
v-model="queryParams.approverName"
|
||||
placeholder="审批人"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<el-form-item label="审批结果" prop="approvalResult">
|
||||
<el-select v-model="queryParams.approvalResult" placeholder="请选择审批结果" clearable style="width: 100%">
|
||||
<el-select v-model="queryParams.approvalResult" placeholder="全部" clearable>
|
||||
<el-option label="通过" value="approved" />
|
||||
<el-option label="驳回" value="rejected" />
|
||||
<el-option label="待审批" value="pending" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="8" :lg="12" :md="16" :sm="24" :xs="24">
|
||||
<el-form-item label="审批时间">
|
||||
<el-form-item label="审批时间" class="form-col-span-2">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
@@ -40,113 +51,188 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24" style="text-align: right">
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按合同、审批人、结果及时间筛选。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="resetQuery">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col v-if="hasContractPerm('approvalExport')" :span="1.5">
|
||||
<el-button type="success" icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table v-loading="loading" :data="approvalList" border>
|
||||
<el-table-column label="合同编号" align="center" prop="contractNo" width="150" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="handleViewContract(scope.row)">{{ scope.row.contractNo }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="合同名称" prop="contractName" :show-overflow-tooltip="true" min-width="200" />
|
||||
<el-table-column label="审批级别" align="center" prop="step" width="100">
|
||||
<template #default="scope"> 第{{ scope.row.step }}级 </template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批人" align="center" prop="approverName" width="120" />
|
||||
<el-table-column label="审批结果" align="center" prop="approvalResult" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.approvalResult === 'approved'" type="success">通过</el-tag>
|
||||
<el-tag v-else-if="scope.row.approvalResult === 'rejected'" type="danger">驳回</el-tag>
|
||||
<el-tag v-else type="warning">待审批</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批意见" prop="approvalOpinion" :show-overflow-tooltip="true" min-width="200" />
|
||||
<el-table-column label="审批时间" align="center" prop="approvalTime" width="160">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.approvalTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-if="hasContractPerm('approvalQuery')" content="查看" placement="top">
|
||||
<el-button link type="primary" icon="View" @click="handleView(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip
|
||||
v-if="hasContractPerm('approvalApprove') && scope.row.approvalResult === 'pending'"
|
||||
content="审批"
|
||||
placement="top"
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<el-button
|
||||
v-if="hasContractPerm('approvalExport')"
|
||||
class="btn-ghost"
|
||||
:loading="exportLoading"
|
||||
@click="handleExport"
|
||||
>
|
||||
<el-button link type="success" icon="CircleCheck" @click="handleApprove(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-icon><Download /></el-icon>导出
|
||||
</el-button>
|
||||
|
||||
<!-- 分页 -->
|
||||
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 查看详情 -->
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewDialog.visible"
|
||||
title="审批详情"
|
||||
:icon="Document"
|
||||
:code="viewDialog.data.contractNo"
|
||||
code-label="合同编号"
|
||||
:meta="viewDialog.data.contractName"
|
||||
:sections="approvalViewSections"
|
||||
append-to-body
|
||||
/>
|
||||
|
||||
<!-- 审批对话框 -->
|
||||
<el-dialog title="审批" v-model="approvalDialog.visible" width="600px" append-to-body>
|
||||
<el-form ref="approvalFormRef" :model="approvalForm" :rules="approvalRules" label-width="100px">
|
||||
<el-form-item label="审批结果" prop="approvalResult">
|
||||
<el-radio-group v-model="approvalForm.approvalResult">
|
||||
<el-radio value="approved">通过</el-radio>
|
||||
<el-radio value="rejected">驳回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审批意见" prop="approvalOpinion">
|
||||
<el-input
|
||||
v-model="approvalForm.approvalOpinion"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:placeholder="approvalForm.approvalResult === 'rejected' ? '请输入驳回原因' : '请输入审批意见(可选)'"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="approvalDialog.visible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitApproval">确 定</el-button>
|
||||
<ListToolbarEndActions :loading="loading" @refresh="getList" />
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
v-loading="loading"
|
||||
:data="approvalList"
|
||||
stripe
|
||||
row-key="id"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column
|
||||
prop="contractNo"
|
||||
label="合同编号"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleViewContract(row)">{{ row.contractNo }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="contractName"
|
||||
label="合同名称"
|
||||
min-width="200"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="审批级别" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">第{{ row.step }}级</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="approverName"
|
||||
label="审批人"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="审批结果" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.approvalResult === 'approved'" type="success" size="small">通过</el-tag>
|
||||
<el-tag v-else-if="row.approvalResult === 'rejected'" type="danger" size="small">驳回</el-tag>
|
||||
<el-tag v-else type="warning" size="small">待审批</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="approvalOpinion"
|
||||
label="审批意见"
|
||||
min-width="200"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="审批时间" min-width="160" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ parseTime(row.approvalTime) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 30, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="getList"
|
||||
@current-change="getList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewDialog.visible"
|
||||
title="审批详情"
|
||||
:icon="Document"
|
||||
:code="viewDialog.data.contractNo"
|
||||
code-label="合同编号"
|
||||
:meta="viewDialog.data.contractName"
|
||||
:sections="approvalViewSections"
|
||||
append-to-body
|
||||
>
|
||||
<template #hero-actions>
|
||||
<div class="approval-view-hero-actions">
|
||||
<el-button
|
||||
v-if="hasContractPerm('approvalApprove') && viewDialog.data.approvalResult === 'pending'"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
@click="handleApprove(viewDialog.data)"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>审批
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="approvalDialog.visible"
|
||||
title="审批"
|
||||
width="600px"
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="approvalFormRef"
|
||||
:model="approvalForm"
|
||||
:rules="approvalRules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="审批结果" prop="approvalResult" class="form-col-span-2">
|
||||
<el-radio-group v-model="approvalForm.approvalResult">
|
||||
<el-radio value="approved">通过</el-radio>
|
||||
<el-radio value="rejected">驳回</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审批意见" prop="approvalOpinion" class="form-col-span-2">
|
||||
<el-input
|
||||
v-model="approvalForm.approvalOpinion"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:placeholder="
|
||||
approvalForm.approvalResult === 'rejected'
|
||||
? '请输入驳回原因'
|
||||
: '请输入审批意见(可选)'
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="approvalDialog.visible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="submitApproval">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ContractApproval">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import { Document, Download, Search, RefreshLeft, Close, Check, EditPen } from '@element-plus/icons-vue'
|
||||
import SystemViewDialog, {
|
||||
type ViewField,
|
||||
type ViewSection
|
||||
@@ -168,54 +254,50 @@ import { useContractPerm } from '@/hooks/business/useContractPerm'
|
||||
const router = useRouter()
|
||||
const { loadPerms, hasContractPerm } = useContractPerm()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref<ApprovalQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
contractNo: undefined,
|
||||
contractName: undefined,
|
||||
approverName: undefined,
|
||||
approvalResult: undefined,
|
||||
beginTime: undefined,
|
||||
endTime: undefined
|
||||
});
|
||||
})
|
||||
|
||||
// 审批表单
|
||||
const approvalForm = ref({
|
||||
id: undefined as number | undefined,
|
||||
approvalResult: 'approved',
|
||||
approvalOpinion: ''
|
||||
});
|
||||
})
|
||||
|
||||
// 审批表单校验规则
|
||||
const approvalRules = ref({
|
||||
approvalResult: [{ required: true, message: '请选择审批结果', trigger: 'change' }],
|
||||
approvalOpinion: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
validator: (_rule: unknown, value: string, callback: (err?: Error) => void) => {
|
||||
if (approvalForm.value.approvalResult === 'rejected' && !value) {
|
||||
callback(new Error('驳回时必须填写驳回原因'));
|
||||
callback(new Error('驳回时必须填写驳回原因'))
|
||||
} else {
|
||||
callback();
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const approvalList = ref<ApprovalVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const total = ref(0);
|
||||
const showSearch = ref(false);
|
||||
const dateRange = ref<[string, string]>();
|
||||
const approvalList = ref<ApprovalVO[]>([])
|
||||
const loading = ref(true)
|
||||
const submitLoading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const total = ref(0)
|
||||
const dateRange = ref<[string, string]>()
|
||||
|
||||
// 查看详情对话框
|
||||
const viewDialog = reactive({
|
||||
visible: false,
|
||||
data: {} as ApprovalVO
|
||||
});
|
||||
})
|
||||
|
||||
function approvalResultTag(result?: string) {
|
||||
if (result === 'approved') return { label: '通过', type: 'success' as const }
|
||||
@@ -252,101 +334,93 @@ const approvalViewSections = computed<ViewSection[]>(() => {
|
||||
]
|
||||
})
|
||||
|
||||
// 审批对话框
|
||||
const approvalDialog = reactive({
|
||||
visible: false
|
||||
});
|
||||
})
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const approvalFormRef = ref<ElFormInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>()
|
||||
const approvalFormRef = ref<ElFormInstance>()
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
try {
|
||||
// 处理日期范围
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
queryParams.value.beginTime = dateRange.value[0];
|
||||
queryParams.value.endTime = dateRange.value[1];
|
||||
queryParams.value.beginTime = dateRange.value[0]
|
||||
queryParams.value.endTime = dateRange.value[1]
|
||||
} else {
|
||||
queryParams.value.beginTime = undefined;
|
||||
queryParams.value.endTime = undefined;
|
||||
queryParams.value.beginTime = undefined
|
||||
queryParams.value.endTime = undefined
|
||||
}
|
||||
|
||||
const res = await listApproval(queryParams.value);
|
||||
approvalList.value = res.rows;
|
||||
total.value = res.total;
|
||||
const res = await listApproval(queryParams.value)
|
||||
approvalList.value = res.rows
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
queryParams.value.pageNum = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
dateRange.value = undefined;
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
dateRange.value = undefined
|
||||
queryFormRef.value?.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
const handleView = async (row: ApprovalVO) => {
|
||||
const res = await getApproval(row.id);
|
||||
viewDialog.data = res.data;
|
||||
viewDialog.visible = true;
|
||||
};
|
||||
const res = await getApproval(row.id)
|
||||
viewDialog.data = res.data
|
||||
viewDialog.visible = true
|
||||
}
|
||||
|
||||
/** 查看合同 */
|
||||
const handleViewContract = (row: ApprovalVO) => {
|
||||
router.push({
|
||||
path: '/crm/contract/detail',
|
||||
query: { id: row.contractId }
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/** 审批操作 */
|
||||
const handleApprove = (row: ApprovalVO) => {
|
||||
approvalForm.value = {
|
||||
id: row.id,
|
||||
approvalResult: 'approved',
|
||||
approvalOpinion: ''
|
||||
};
|
||||
approvalDialog.visible = true;
|
||||
};
|
||||
}
|
||||
approvalDialog.visible = true
|
||||
}
|
||||
|
||||
/** 提交审批 */
|
||||
const submitApproval = () => {
|
||||
approvalFormRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (approvalForm.value.approvalResult === 'approved') {
|
||||
await approvePass({
|
||||
id: approvalForm.value.id!,
|
||||
approvalOpinion: approvalForm.value.approvalOpinion
|
||||
});
|
||||
$modal.msgSuccess('审批通过')
|
||||
} else {
|
||||
await approveReject({
|
||||
id: approvalForm.value.id!,
|
||||
approvalOpinion: approvalForm.value.approvalOpinion
|
||||
})
|
||||
$modal.msgSuccess('已驳回')
|
||||
}
|
||||
approvalDialog.visible = false;
|
||||
await getList();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (approvalForm.value.approvalResult === 'approved') {
|
||||
await approvePass({
|
||||
id: approvalForm.value.id!,
|
||||
approvalOpinion: approvalForm.value.approvalOpinion
|
||||
})
|
||||
$modal.msgSuccess('审批通过')
|
||||
} else {
|
||||
await approveReject({
|
||||
id: approvalForm.value.id!,
|
||||
approvalOpinion: approvalForm.value.approvalOpinion
|
||||
})
|
||||
$modal.msgSuccess('已驳回')
|
||||
}
|
||||
approvalDialog.visible = false
|
||||
await getList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = async () => {
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
queryParams.value.beginTime = dateRange.value[0]
|
||||
@@ -355,6 +429,7 @@ const handleExport = async () => {
|
||||
queryParams.value.beginTime = undefined
|
||||
queryParams.value.endTime = undefined
|
||||
}
|
||||
exportLoading.value = true
|
||||
try {
|
||||
const res = await exportApproval(queryParams.value)
|
||||
const blob =
|
||||
@@ -372,6 +447,8 @@ const handleExport = async () => {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$modal.msgError('导出失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,10 +457,3 @@ onMounted(async () => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.text-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +1,32 @@
|
||||
<!-- 合同变更记录 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 查询表单 -->
|
||||
<el-card shadow="hover" v-show="showSearch" class="mb-[10px]">
|
||||
<el-form :model="queryParams" ref="queryFormRef" label-width="90px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">合同变更</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form ref="queryFormRef" :model="queryParams" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="合同编号" prop="contractNo">
|
||||
<el-input v-model="queryParams.contractNo" placeholder="请输入合同编号" clearable @keyup.enter="handleQuery" />
|
||||
<el-input
|
||||
v-model="queryParams.contractNo"
|
||||
placeholder="合同编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<el-form-item label="合同名称" prop="contractName">
|
||||
<el-input v-model="queryParams.contractName" placeholder="请输入合同名称" clearable @keyup.enter="handleQuery" />
|
||||
<el-input
|
||||
v-model="queryParams.contractName"
|
||||
placeholder="合同名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<el-form-item label="变更类型" prop="changeType">
|
||||
<el-select v-model="queryParams.changeType" placeholder="请选择变更类型" clearable style="width: 100%">
|
||||
<el-select v-model="queryParams.changeType" placeholder="全部" clearable>
|
||||
<el-option
|
||||
v-for="dict in dictStore.getOptions('crm_contract_change_type')"
|
||||
:key="dict.value"
|
||||
@@ -25,18 +35,14 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<el-form-item label="审批状态" prop="approvalStatus">
|
||||
<el-select v-model="queryParams.approvalStatus" placeholder="请选择审批状态" clearable style="width: 100%">
|
||||
<el-select v-model="queryParams.approvalStatus" placeholder="全部" clearable>
|
||||
<el-option label="待审批" value="pending" />
|
||||
<el-option label="已通过" value="approved" />
|
||||
<el-option label="已驳回" value="rejected" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="8" :lg="12" :md="16" :sm="24" :xs="24">
|
||||
<el-form-item label="变更时间">
|
||||
<el-form-item label="变更时间" class="form-col-span-2">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
@@ -47,173 +53,308 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24" style="text-align: right">
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按合同、变更类型、审批状态及时间筛选。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="resetQuery">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col v-if="hasContractPerm('changeAdd')" :span="1.5">
|
||||
<el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('changeRemove')" :span="1.5">
|
||||
<el-button type="danger" icon="Delete" :disabled="multiple || !allRowsDeletableByCreator(changeList.filter((item) => ids.includes(item.id)))" :title="allRowsDeletableByCreator(changeList.filter((item) => ids.includes(item.id))) ? '' : CREATOR_DELETE_TIP" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('changeExport')" :span="1.5">
|
||||
<el-button type="success" icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table v-loading="loading" :data="changeList" @selection-change="handleSelectionChange" border>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="合同编号" align="center" prop="contractNo" width="150" :show-overflow-tooltip="true">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="handleViewContract(scope.row)">{{ scope.row.contractNo }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="合同名称" prop="contractName" :show-overflow-tooltip="true" min-width="180" />
|
||||
<el-table-column label="变更类型" align="center" prop="changeType" width="120">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_contract_change_type" :value="scope.row.changeType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变更原因" prop="changeReason" :show-overflow-tooltip="true" min-width="200" />
|
||||
<el-table-column label="变更金额" align="right" prop="changeAmount" width="130">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.changeAmount">¥{{ scope.row.changeAmount.toLocaleString() }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批状态" align="center" prop="approvalStatus" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.approvalStatus === 'pending'" type="warning">待审批</el-tag>
|
||||
<el-tag v-else-if="scope.row.approvalStatus === 'approved'" type="success">已通过</el-tag>
|
||||
<el-tag v-else-if="scope.row.approvalStatus === 'rejected'" type="danger">已驳回</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批人" align="center" prop="approverName" width="100" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-if="hasContractPerm('changeQuery')" content="查看" placement="top">
|
||||
<el-button link type="primary" icon="View" @click="handleView(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-dropdown
|
||||
v-if="hasContractPerm('changeApprove') || hasContractPerm('changeRemove')"
|
||||
@command="(command) => handleCommand(command, scope.row)"
|
||||
trigger="click"
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button
|
||||
v-if="hasContractPerm('changeAdd')"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
<el-button link type="primary" icon="MoreFilled" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-if="hasContractPerm('changeApprove') && scope.row.approvalStatus === 'pending'"
|
||||
command="submit"
|
||||
icon="Promotion"
|
||||
>提交审批</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="hasContractPerm('changeApprove') && scope.row.approvalStatus === 'approved'"
|
||||
command="execute"
|
||||
icon="VideoPlay"
|
||||
>执行变更</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="hasContractPerm('changeApprove') && scope.row.approvalStatus === 'pending'"
|
||||
command="withdraw"
|
||||
icon="RefreshLeft"
|
||||
>撤回</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="hasContractPerm('changeRemove') && scope.row.approvalStatus === 'pending' && canDeleteByCreator(scope.row)"
|
||||
command="delete"
|
||||
icon="Delete"
|
||||
divided
|
||||
>删除</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-icon><Plus /></el-icon>新增变更
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('changeRemove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('changeApprove')"
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1 || selectedRows[0]?.approvalStatus !== 'pending'"
|
||||
@click="handleSubmitApproval(selectedRows[0])"
|
||||
>
|
||||
<el-icon><Promotion /></el-icon>提交审批
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('changeApprove')"
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1 || selectedRows[0]?.approvalStatus !== 'approved'"
|
||||
@click="handleExecute(selectedRows[0])"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon>执行变更
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('changeApprove')"
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1 || selectedRows[0]?.approvalStatus !== 'pending'"
|
||||
@click="handleWithdraw(selectedRows[0])"
|
||||
>
|
||||
<el-icon><RefreshLeft /></el-icon>撤回
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('changeExport')"
|
||||
class="btn-ghost"
|
||||
:loading="exportLoading"
|
||||
@click="handleExport"
|
||||
>
|
||||
<el-icon><Download /></el-icon>导出
|
||||
</el-button>
|
||||
|
||||
<!-- 分页 -->
|
||||
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
|
||||
<!-- 添加或修改对话框 -->
|
||||
<el-dialog :title="dialog.title" v-model="dialog.visible" width="800px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="合同" prop="contractId">
|
||||
<el-select v-model="form.contractId" placeholder="请选择合同" filterable style="width: 100%">
|
||||
<!-- TODO: 动态加载合同列表 -->
|
||||
<el-option label="合同1" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="变更类型" prop="changeType">
|
||||
<el-select v-model="form.changeType" placeholder="请选择变更类型" style="width: 100%">
|
||||
<el-option
|
||||
v-for="dict in dictStore.getOptions('crm_contract_change_type')"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="变更原因" prop="changeReason">
|
||||
<el-input v-model="form.changeReason" type="textarea" :rows="3" placeholder="请输入变更原因" />
|
||||
</el-form-item>
|
||||
<el-form-item label="变更内容" prop="changeContent">
|
||||
<el-input v-model="form.changeContent" type="textarea" :rows="2" placeholder="请输入变更内容描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="变更前值" prop="oldValue">
|
||||
<el-input v-model="form.oldValue" type="textarea" :rows="2" placeholder="请输入变更前的值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="变更后值" prop="newValue">
|
||||
<el-input v-model="form.newValue" type="textarea" :rows="2" placeholder="请输入变更后的值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="变更金额" prop="changeAmount">
|
||||
<el-input-number v-model="form.changeAmount" :precision="2" :min="0" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<ListToolbarEndActions :loading="loading" @refresh="getList" />
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<!-- 查看详情 -->
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewDialog.visible"
|
||||
title="变更详情"
|
||||
:icon="Document"
|
||||
:code="viewDialog.data.contractNo"
|
||||
code-label="合同编号"
|
||||
:meta="viewDialog.data.contractName"
|
||||
:sections="changeViewSections"
|
||||
append-to-body
|
||||
/>
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
v-loading="loading"
|
||||
:data="changeList"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
prop="contractNo"
|
||||
label="合同编号"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleViewContract(row)">{{ row.contractNo }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="contractName"
|
||||
label="合同名称"
|
||||
min-width="180"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="变更类型" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="dictStore.getTagType('crm_contract_change_type', row.changeType)"
|
||||
size="small"
|
||||
>
|
||||
{{ dictStore.getLabel('crm_contract_change_type', row.changeType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="changeReason"
|
||||
label="变更原因"
|
||||
min-width="200"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="变更金额" 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">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.approvalStatus === 'pending'" type="warning" size="small">待审批</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 'approved'" type="success" size="small">已通过</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 'rejected'" type="danger" size="small">已驳回</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="approverName"
|
||||
label="审批人"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column label="创建时间" min-width="160" align="center" header-align="center">
|
||||
<template #default="{ row }">{{ parseTime(row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 30, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="getList"
|
||||
@current-change="getList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialog.visible"
|
||||
:title="dialog.title"
|
||||
width="800px"
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
@close="cancel"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="合同" prop="contractId" class="form-col-span-2">
|
||||
<el-select v-model="form.contractId" placeholder="请选择合同" filterable style="width: 100%">
|
||||
<!-- TODO: 动态加载合同列表 -->
|
||||
<el-option label="合同1" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="变更类型" prop="changeType">
|
||||
<el-select v-model="form.changeType" placeholder="请选择变更类型" style="width: 100%">
|
||||
<el-option
|
||||
v-for="dict in dictStore.getOptions('crm_contract_change_type')"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="变更金额" prop="changeAmount">
|
||||
<el-input-number
|
||||
v-model="form.changeAmount"
|
||||
:precision="2"
|
||||
:min="0"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="变更原因" prop="changeReason" class="form-col-span-2">
|
||||
<el-input v-model="form.changeReason" type="textarea" :rows="3" placeholder="请输入变更原因" />
|
||||
</el-form-item>
|
||||
<el-form-item label="变更内容" prop="changeContent" class="form-col-span-2">
|
||||
<el-input v-model="form.changeContent" type="textarea" :rows="2" placeholder="请输入变更内容描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="变更前值" prop="oldValue" class="form-col-span-2">
|
||||
<el-input v-model="form.oldValue" type="textarea" :rows="2" placeholder="请输入变更前的值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="变更后值" prop="newValue" class="form-col-span-2">
|
||||
<el-input v-model="form.newValue" type="textarea" :rows="2" placeholder="请输入变更后的值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark" class="form-col-span-2">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="cancel">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="submitForm">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewDialog.visible"
|
||||
title="变更详情"
|
||||
:icon="Document"
|
||||
:code="viewDialog.data.contractNo"
|
||||
code-label="合同编号"
|
||||
:meta="viewDialog.data.contractName"
|
||||
:sections="changeViewSections"
|
||||
append-to-body
|
||||
>
|
||||
<template #hero-actions>
|
||||
<div class="change-view-hero-actions">
|
||||
<el-button
|
||||
v-if="hasContractPerm('changeApprove') && viewDialog.data.approvalStatus === 'pending'"
|
||||
class="btn-ghost"
|
||||
@click="handleSubmitApproval(viewDialog.data)"
|
||||
>
|
||||
<el-icon><Promotion /></el-icon>提交审批
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('changeApprove') && viewDialog.data.approvalStatus === 'approved'"
|
||||
class="btn-ghost"
|
||||
@click="handleExecute(viewDialog.data)"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon>执行变更
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('changeApprove') && viewDialog.data.approvalStatus === 'pending'"
|
||||
class="btn-ghost"
|
||||
@click="handleWithdraw(viewDialog.data)"
|
||||
>
|
||||
<el-icon><RefreshLeft /></el-icon>撤回
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="
|
||||
hasContractPerm('changeRemove') &&
|
||||
viewDialog.data.approvalStatus === 'pending' &&
|
||||
canDeleteByCreator(viewDialog.data)
|
||||
"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
@click="handleDetailDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ContractChange">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Document,
|
||||
Plus,
|
||||
Delete,
|
||||
Download,
|
||||
Search,
|
||||
RefreshLeft,
|
||||
Close,
|
||||
Check,
|
||||
Promotion,
|
||||
VideoPlay
|
||||
} from '@element-plus/icons-vue'
|
||||
import SystemViewDialog, {
|
||||
type ViewField,
|
||||
type ViewSection
|
||||
@@ -235,28 +376,30 @@ import {
|
||||
} from '@/api/crm/contract/change'
|
||||
import { parseTime } from '@/utils/format'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
canDeleteByCreator,
|
||||
filterDeletableByCreator,
|
||||
allRowsDeletableByCreator,
|
||||
CREATOR_DELETE_TIP
|
||||
} from '@/utils/creator-delete'
|
||||
|
||||
const router = useRouter()
|
||||
const dictStore = useDictStore()
|
||||
const { loadPerms, hasContractPerm } = useContractPerm()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref<ChangeQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
contractNo: undefined,
|
||||
contractName: undefined,
|
||||
changeType: undefined,
|
||||
approvalStatus: undefined,
|
||||
beginTime: undefined,
|
||||
endTime: undefined
|
||||
});
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const form = ref<ChangeForm>({
|
||||
contractId: 0,
|
||||
changeType: '',
|
||||
@@ -266,37 +409,33 @@ const form = ref<ChangeForm>({
|
||||
newValue: '',
|
||||
changeAmount: 0,
|
||||
remark: ''
|
||||
});
|
||||
})
|
||||
|
||||
// 表单校验规则
|
||||
const rules = ref({
|
||||
contractId: [{ required: true, message: '请选择合同', trigger: 'change' }],
|
||||
changeType: [{ required: true, message: '请选择变更类型', trigger: 'change' }],
|
||||
changeReason: [{ required: true, message: '请输入变更原因', trigger: 'blur' }],
|
||||
newValue: [{ required: true, message: '请输入变更后的值', trigger: 'blur' }]
|
||||
});
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const changeList = ref<ChangeVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const total = ref(0);
|
||||
const showSearch = ref(false);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const ids = ref<number[]>([]);
|
||||
const dateRange = ref<[string, string]>();
|
||||
const changeList = ref<ChangeVO[]>([])
|
||||
const selectedRows = ref<ChangeVO[]>([])
|
||||
const loading = ref(true)
|
||||
const submitLoading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const total = ref(0)
|
||||
const ids = ref<number[]>([])
|
||||
const dateRange = ref<[string, string]>()
|
||||
|
||||
// 对话框
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
})
|
||||
|
||||
// 查看详情对话框
|
||||
const viewDialog = reactive({
|
||||
visible: false,
|
||||
data: {} as ChangeVO
|
||||
});
|
||||
})
|
||||
|
||||
function changeStatusTag(status?: string) {
|
||||
if (status === 'pending') return { label: '待审批', type: 'warning' as const }
|
||||
@@ -358,105 +497,87 @@ const changeViewSections = computed<ViewSection[]>(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const formRef = ref<ElFormInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>()
|
||||
const formRef = ref<ElFormInstance>()
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
try {
|
||||
// 处理日期范围
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
queryParams.value.beginTime = dateRange.value[0];
|
||||
queryParams.value.endTime = dateRange.value[1];
|
||||
queryParams.value.beginTime = dateRange.value[0]
|
||||
queryParams.value.endTime = dateRange.value[1]
|
||||
} else {
|
||||
queryParams.value.beginTime = undefined;
|
||||
queryParams.value.endTime = undefined;
|
||||
queryParams.value.beginTime = undefined
|
||||
queryParams.value.endTime = undefined
|
||||
}
|
||||
|
||||
const res = await listChange(queryParams.value);
|
||||
changeList.value = res.rows;
|
||||
total.value = res.total;
|
||||
const res = await listChange(queryParams.value)
|
||||
changeList.value = res.rows
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
queryParams.value.pageNum = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
dateRange.value = undefined;
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
dateRange.value = undefined
|
||||
queryFormRef.value?.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: ChangeVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
single.value = selection.length !== 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
selectedRows.value = selection
|
||||
ids.value = selection.map((item) => item.id)
|
||||
}
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加变更记录';
|
||||
};
|
||||
reset()
|
||||
dialog.visible = true
|
||||
dialog.title = '添加变更记录'
|
||||
}
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row: ChangeVO) => {
|
||||
reset();
|
||||
const res = await getChange(row.id);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改变更记录';
|
||||
};
|
||||
|
||||
/** 查看详情 */
|
||||
const handleView = async (row: ChangeVO) => {
|
||||
const res = await getChange(row.id);
|
||||
viewDialog.data = res.data;
|
||||
viewDialog.visible = true;
|
||||
};
|
||||
const res = await getChange(row.id)
|
||||
viewDialog.data = res.data
|
||||
viewDialog.visible = true
|
||||
}
|
||||
|
||||
/** 查看合同 */
|
||||
const handleViewContract = (row: ChangeVO) => {
|
||||
router.push({
|
||||
path: '/crm/contract/detail',
|
||||
query: { id: row.contractId }
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
const submitForm = () => {
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (form.value.id) {
|
||||
await updateChange(form.value);
|
||||
$modal.msgSuccess('修改成功')
|
||||
} else {
|
||||
await addChange(form.value)
|
||||
$modal.msgSuccess('新增成功')
|
||||
}
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (form.value.id) {
|
||||
await updateChange(form.value)
|
||||
$modal.msgSuccess('修改成功')
|
||||
} else {
|
||||
await addChange(form.value)
|
||||
$modal.msgSuccess('新增成功')
|
||||
}
|
||||
dialog.visible = false
|
||||
await getList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: ChangeVO) => {
|
||||
const targets = row ? [row] : changeList.value.filter((item) => ids.value.includes(item.id))
|
||||
const targets = row ? [row] : selectedRows.value
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
@@ -466,57 +587,57 @@ const handleDelete = async (row?: ChangeVO) => {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const idList = row?.id ? [row.id] : ids.value;
|
||||
const idList = row?.id ? [row.id] : ids.value
|
||||
await $modal.confirm('是否确认删除选中的变更记录?')
|
||||
await delChange(idList)
|
||||
$modal.msgSuccess('删除成功')
|
||||
await getList();
|
||||
};
|
||||
await getList()
|
||||
}
|
||||
|
||||
async function handleDetailDelete() {
|
||||
if (!viewDialog.data?.id) return
|
||||
await handleDelete(viewDialog.data as ChangeVO)
|
||||
viewDialog.visible = false
|
||||
}
|
||||
|
||||
/** 下拉菜单命令处理 */
|
||||
const handleCommand = async (command: string, row: ChangeVO) => {
|
||||
switch (command) {
|
||||
case 'submit':
|
||||
await handleSubmitApproval(row);
|
||||
break;
|
||||
await handleSubmitApproval(row)
|
||||
break
|
||||
case 'execute':
|
||||
await handleExecute(row);
|
||||
break;
|
||||
await handleExecute(row)
|
||||
break
|
||||
case 'withdraw':
|
||||
await handleWithdraw(row);
|
||||
break;
|
||||
await handleWithdraw(row)
|
||||
break
|
||||
case 'delete':
|
||||
await handleDelete(row);
|
||||
break;
|
||||
await handleDelete(row)
|
||||
break
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 提交审批 */
|
||||
const handleSubmitApproval = async (row: ChangeVO) => {
|
||||
await $modal.confirm('是否确认提交该变更记录进行审批?')
|
||||
// TODO: 选择审批人
|
||||
await submitChangeApproval({ id: row.id, approverIds: [] })
|
||||
$modal.msgSuccess('提交成功')
|
||||
await getList();
|
||||
};
|
||||
await getList()
|
||||
}
|
||||
|
||||
/** 执行变更 */
|
||||
const handleExecute = async (row: ChangeVO) => {
|
||||
await $modal.confirm('是否确认执行该变更?执行后将无法撤销')
|
||||
await executeChange(row.id)
|
||||
$modal.msgSuccess('执行成功')
|
||||
await getList();
|
||||
};
|
||||
await getList()
|
||||
}
|
||||
|
||||
/** 撤回变更 */
|
||||
const handleWithdraw = async (row: ChangeVO) => {
|
||||
await $modal.confirm('是否确认撤回该变更记录?')
|
||||
await withdrawChange(row.id)
|
||||
$modal.msgSuccess('撤回成功')
|
||||
await getList();
|
||||
};
|
||||
await getList()
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = async () => {
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
queryParams.value.beginTime = dateRange.value[0]
|
||||
@@ -525,6 +646,7 @@ const handleExport = async () => {
|
||||
queryParams.value.beginTime = undefined
|
||||
queryParams.value.endTime = undefined
|
||||
}
|
||||
exportLoading.value = true
|
||||
try {
|
||||
const res = await exportChange(queryParams.value)
|
||||
const blob =
|
||||
@@ -542,16 +664,16 @@ const handleExport = async () => {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$modal.msgError('导出失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
dialog.visible = false;
|
||||
reset();
|
||||
};
|
||||
dialog.visible = false
|
||||
reset()
|
||||
}
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = {
|
||||
id: undefined,
|
||||
@@ -563,19 +685,12 @@ const reset = () => {
|
||||
newValue: '',
|
||||
changeAmount: 0,
|
||||
remark: ''
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadPerms()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.text-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -362,7 +362,13 @@
|
||||
</div>
|
||||
<div class="list-body list-wrap">
|
||||
<el-table :data="detailPaymentList" class="list-table" stripe v-loading="detailPaymentLoading" empty-text="暂无回款记录">
|
||||
<el-table-column label="回款单号" prop="paymentNo" min-width="150" align="center" header-align="center" />
|
||||
<el-table-column label="回款单号" prop="paymentNo" min-width="150" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="handleDetailViewPayment(scope.row)">
|
||||
{{ scope.row.paymentNo }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回款金额" prop="amount" min-width="130" align="right">
|
||||
<template #default="scope">
|
||||
<span class="text-success font-weight-bold"
|
||||
@@ -376,13 +382,6 @@
|
||||
<el-table-column label="创建人" prop="createBy" width="100" align="center" />
|
||||
<el-table-column label="创建时间" prop="createTime" width="160" align="center">
|
||||
<template #default="scope">{{ parseTime(scope.row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="120" align="center" header-align="center" fixed="right" class-name="col-actions">
|
||||
<template #default="scope">
|
||||
<el-button link type="success" @click="handleDetailViewPayment(scope.row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
@@ -403,7 +402,13 @@
|
||||
</div>
|
||||
<div class="list-body list-wrap">
|
||||
<el-table :data="detailInvoiceList" class="list-table" stripe v-loading="detailInvoiceLoading" empty-text="暂无开票记录">
|
||||
<el-table-column label="发票号码" prop="invoiceNo" min-width="150" align="center" header-align="center" />
|
||||
<el-table-column label="发票号码" prop="invoiceNo" min-width="150" align="center" header-align="center">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="handleDetailViewInvoice(scope.row)">
|
||||
{{ scope.row.invoiceNo }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发票类型" prop="invoiceType" width="120" align="center" />
|
||||
<el-table-column label="开票金额" prop="amount" min-width="130" align="right">
|
||||
<template #default="scope">
|
||||
@@ -417,13 +422,6 @@
|
||||
<el-table-column label="创建人" prop="createBy" width="100" align="center" />
|
||||
<el-table-column label="创建时间" prop="createTime" width="160" align="center">
|
||||
<template #default="scope">{{ parseTime(scope.row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="120" align="center" header-align="center" fixed="right" class-name="col-actions">
|
||||
<template #default="scope">
|
||||
<el-button link type="success" @click="handleDetailViewInvoice(scope.row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -188,7 +188,7 @@ watch(dialogVisible, (val) => {
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
productName: undefined,
|
||||
categoryId: undefined,
|
||||
status: 'on' // on-上架 off-下架
|
||||
|
||||
@@ -1,84 +1,81 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-page-header @back="goBack" content="合同详情" />
|
||||
|
||||
<el-card class="mt-4" v-loading="loading">
|
||||
<!-- 头部操作按钮 -->
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div class="header-left">
|
||||
<span class="card-title">{{ contract?.contractNo }} - {{ contract?.contractName }}</span>
|
||||
<div class="page-container contract-detail-page">
|
||||
<section class="panel" v-loading="loading">
|
||||
<div class="panel-top panel-top-with-actions">
|
||||
<div class="panel-top-main">
|
||||
<h2 class="title-bar-heading">合同详情</h2>
|
||||
<div v-if="contract" class="header-left header-tags">
|
||||
<el-tag
|
||||
v-if="contract?.status"
|
||||
:type="dictStore.getTagType('crm_contract_status', contract.status)"
|
||||
size="large"
|
||||
class="ml-2"
|
||||
size="small"
|
||||
>{{ dictStore.getLabel('crm_contract_status', contract.status) }}</el-tag>
|
||||
<el-tag
|
||||
v-if="contract?.approvalStatus"
|
||||
:type="dictStore.getTagType('crm_approval_status', contract.approvalStatus)"
|
||||
size="large"
|
||||
class="ml-2"
|
||||
size="small"
|
||||
>{{ dictStore.getLabel('crm_approval_status', contract.approvalStatus) }}</el-tag>
|
||||
</div>
|
||||
<div>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="Edit"
|
||||
@click="handleEdit"
|
||||
v-if="hasContractPerm('edit') && contract?.status === 'draft'"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('remove') && canDeleteByCreator(contract)"
|
||||
type="danger"
|
||||
icon="Delete"
|
||||
@click="handleDelete"
|
||||
>删除</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
icon="Promotion"
|
||||
@click="handleSubmit"
|
||||
v-if="hasContractPerm('submit') && contract?.status === 'draft'"
|
||||
>提交审批</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
icon="VideoPlay"
|
||||
@click="handleExecute"
|
||||
v-if="hasContractPerm('execute') && contract?.status === 'approved' && contract?.approvalStatus === 'approved'"
|
||||
>开始执行</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="CircleCheck"
|
||||
@click="handleComplete"
|
||||
v-if="hasContractPerm('finish') && contract?.status === 'executing'"
|
||||
>完成合同</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
icon="CircleClose"
|
||||
@click="handleInvalid"
|
||||
v-if="hasContractPerm('invalid') && ['draft', 'approving', 'approved', 'executing'].includes(contract?.status || '')"
|
||||
>作废</el-button
|
||||
>
|
||||
<el-button
|
||||
icon="Printer"
|
||||
@click="handlePrint"
|
||||
v-if="hasContractPerm('print')"
|
||||
:disabled="contract?.contractTemplate === 'customer'"
|
||||
>打印</el-button>
|
||||
<!-- <el-button
|
||||
type="warning"
|
||||
icon="RefreshRight"
|
||||
@click="handleRenew"
|
||||
v-if="['executing', 'finished'].includes(contract?.status || '')"
|
||||
|
||||
>发起续签</el-button
|
||||
> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="detail-toolbar">
|
||||
<el-button class="btn-ghost" @click="goBack">
|
||||
<el-icon><Back /></el-icon>返回
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleEdit"
|
||||
v-if="hasContractPerm('edit') && contract?.status === 'draft'"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>修改
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('remove') && canDeleteByCreator(contract)"
|
||||
type="danger"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="handleSubmit"
|
||||
v-if="hasContractPerm('submit') && contract?.status === 'draft'"
|
||||
>
|
||||
<el-icon><Promotion /></el-icon>提交审批
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@click="handleExecute"
|
||||
v-if="hasContractPerm('execute') && contract?.status === 'approved' && contract?.approvalStatus === 'approved'"
|
||||
>
|
||||
<el-icon><VideoPlay /></el-icon>开始执行
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleComplete"
|
||||
v-if="hasContractPerm('finish') && contract?.status === 'executing'"
|
||||
>
|
||||
<el-icon><CircleCheck /></el-icon>完成合同
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="handleInvalid"
|
||||
v-if="hasContractPerm('invalid') && ['draft', 'approving', 'approved', 'executing'].includes(contract?.status || '')"
|
||||
>
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="handlePrint"
|
||||
v-if="hasContractPerm('print')"
|
||||
:disabled="contract?.contractTemplate === 'customer'"
|
||||
>
|
||||
<el-icon><Printer /></el-icon>打印
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<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-tab-pane label="合同信息" name="contract">
|
||||
<el-form label-width="120px" v-if="contract">
|
||||
@@ -956,7 +953,7 @@
|
||||
<el-empty v-if="operLogList.length === 0 && !logLoading" description="暂无操作日志" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</section>
|
||||
|
||||
<!-- 审批对话框 -->
|
||||
<ApprovalDialog ref="approvalDialogRef" @success="loadContractDetail" />
|
||||
@@ -1009,7 +1006,7 @@ import { $modal } from '@/utils/modal';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import DictTag from '@/components/DictTag/index.vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { CircleCheck, CircleClose, View, ChatLineSquare, Clock, InfoFilled, VideoPlay, Document, Paperclip, Download } from '@element-plus/icons-vue';
|
||||
import { CircleCheck, CircleClose, View, ChatLineSquare, Clock, InfoFilled, VideoPlay, Document, Paperclip, Download, Back, Edit, Delete, Promotion, Printer } from '@element-plus/icons-vue';
|
||||
import ApprovalDialog from './components/ApprovalDialog.vue';
|
||||
import ExecuteDialog from './components/ExecuteDialog.vue';
|
||||
import InvalidDialog from './components/InvalidDialog.vue';
|
||||
@@ -1064,7 +1061,7 @@ const logLoading = ref(false);
|
||||
const logTotal = ref(0);
|
||||
const logQuery = ref<OperLogQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
pageSize: 20
|
||||
} as OperLogQuery);
|
||||
|
||||
// 组件引用
|
||||
@@ -2649,6 +2646,29 @@ const handleGeneratePdfFile = async () => {
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.contract-detail-page {
|
||||
.detail-subtitle {
|
||||
margin: 0;
|
||||
padding: 0 16px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
.header-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.detail-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -2697,7 +2717,7 @@ const handleGeneratePdfFile = async () => {
|
||||
font-weight: bold;
|
||||
|
||||
&.primary {
|
||||
color: #409eff;
|
||||
color: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">{{ formTitle }}</span>
|
||||
<el-button style="float: right" @click="handleBack">返回</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 步骤条已移除 -->
|
||||
<div class="page-container contract-form-page">
|
||||
<section class="panel">
|
||||
<div class="panel-top panel-top-with-actions">
|
||||
<h2 class="title-bar-heading">{{ formTitle }}</h2>
|
||||
<el-button class="btn-ghost" @click="handleBack">
|
||||
<el-icon><Back /></el-icon>返回
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<!-- 合同信息(融合基本信息+合同条款+产品明细) -->
|
||||
@@ -535,7 +533,7 @@
|
||||
<el-button @click="handleBack">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</section>
|
||||
|
||||
<!-- 产品选择器 -->
|
||||
<ProductSelector :visible="productSelectorVisible" @update:visible="productSelectorVisible = $event" @confirm="handleProductSelected" />
|
||||
@@ -583,7 +581,7 @@ import {
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { $modal } from '@/utils/modal';
|
||||
import { UploadFilled, Document, Loading, Download, Delete, ZoomIn, Sort, Plus } from '@element-plus/icons-vue';
|
||||
import { UploadFilled, Document, Loading, Download, Delete, ZoomIn, Sort, Plus, Back } from '@element-plus/icons-vue';
|
||||
import ImagePreview from '@/components/ImagePreview/index.vue';
|
||||
import {
|
||||
buildUploadFileListFromField,
|
||||
@@ -2494,8 +2492,9 @@ onMounted(async () => {
|
||||
.total-section {
|
||||
margin-top: 20px;
|
||||
padding: 15px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
background-color: var(--mint-row-hover, #f1f5f9);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
}
|
||||
|
||||
.total-item {
|
||||
|
||||
@@ -5,17 +5,19 @@
|
||||
<h2 class="title-bar-heading">合同管理</h2>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs">
|
||||
<el-tabs v-model="activeView" @tab-change="handleViewChange">
|
||||
<el-tab-pane label="我的合同" name="my" />
|
||||
<el-tab-pane label="全部合同" name="all" />
|
||||
<el-tab-pane label="待审批" name="submitted" />
|
||||
<el-tab-pane label="已审批" name="approved" />
|
||||
<el-tab-pane label="执行中" name="executing" />
|
||||
<el-tab-pane label="已完成" name="finished" />
|
||||
<el-tab-pane label="已作废" name="invalid" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-tabs
|
||||
v-model="activeView"
|
||||
class="panel-pill-tabs panel-pill-tabs--lg"
|
||||
@tab-change="handleViewChange"
|
||||
>
|
||||
<el-tab-pane label="我的合同" name="my" />
|
||||
<el-tab-pane label="全部合同" name="all" />
|
||||
<el-tab-pane label="待审批" name="submitted" />
|
||||
<el-tab-pane label="已审批" name="approved" />
|
||||
<el-tab-pane label="执行中" name="executing" />
|
||||
<el-tab-pane label="已完成" name="finished" />
|
||||
<el-tab-pane label="已作废" name="invalid" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form ref="queryFormRef" :model="queryParams" class="filter-form" @submit.prevent>
|
||||
@@ -155,6 +157,28 @@
|
||||
<el-button v-if="hasContractPerm('add')" class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('edit')"
|
||||
class="btn-ghost"
|
||||
:disabled="single || selectedRows[0]?.status !== 'draft'"
|
||||
@click="handleUpdate()"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('remove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="
|
||||
multiple ||
|
||||
!selectedRows.every((row) => row.status === 'draft') ||
|
||||
!allRowsDeletableByCreator(selectedRows)
|
||||
"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('submit') && activeView === 'my'"
|
||||
class="btn-ghost"
|
||||
@@ -190,12 +214,16 @@
|
||||
<el-button v-if="hasContractPerm('export')" class="btn-ghost" :loading="exportLoading" @click="handleExport">
|
||||
<el-icon><Download /></el-icon>导出
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handleColumnSetting">
|
||||
<el-icon><Setting /></el-icon>列设置
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="loading" @click="getList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -443,23 +471,6 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column v-if="isColumnVisible('remark')" label="备注" align="center" prop="remark" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" min-width="260" align="center" header-align="center" fixed="right" class-name="col-actions">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button v-if="hasContractPerm('query')" class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('edit')"
|
||||
class="btn-op-item primary"
|
||||
:disabled="row.status !== 'draft'"
|
||||
@click="handleUpdate(row)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -492,38 +503,6 @@
|
||||
<ContractDetailDialog ref="contractDetailRef" @success="getList" />
|
||||
|
||||
|
||||
<!-- 列设置对话框 -->
|
||||
<el-dialog
|
||||
v-model="columnSettingDialog.visible"
|
||||
title="列设置"
|
||||
width="600px"
|
||||
align-center
|
||||
append-to-body
|
||||
class="dialog-form-layout"
|
||||
modal-class="order-module-dialog-mask"
|
||||
>
|
||||
<div class="column-setting-content">
|
||||
<el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" @change="handleCheckAllChange">全选</el-checkbox>
|
||||
<el-divider />
|
||||
<el-checkbox-group v-model="visibleColumns" @change="handleCheckedColumnsChange">
|
||||
<div v-for="col in tableColumns" :key="col.prop" class="column-item">
|
||||
<el-checkbox :label="col.prop" :disabled="col.fixed">{{ col.label }}</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="handleResetColumns">
|
||||
<el-icon><RefreshLeft /></el-icon>恢复默认
|
||||
</el-button>
|
||||
<el-button @click="columnSettingDialog.visible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleSaveColumnSetting">
|
||||
<el-icon><Check /></el-icon>保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 合同表单对话框 -->
|
||||
<ContractFormDialog
|
||||
v-model:visible="formDialogVisible"
|
||||
@@ -546,7 +525,6 @@ import {
|
||||
import {
|
||||
ArrowDownBold,
|
||||
ArrowUpBold,
|
||||
Check,
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
Clock,
|
||||
@@ -556,13 +534,10 @@ import {
|
||||
Download,
|
||||
Edit,
|
||||
Plus,
|
||||
Refresh,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
Select,
|
||||
Setting,
|
||||
Upload,
|
||||
View
|
||||
Upload
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
const dictStore = useDictStore()
|
||||
@@ -571,7 +546,7 @@ import { $modal } from '@/utils/modal'
|
||||
import { parseTime } from '@/utils/format'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm'
|
||||
import { filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import ApprovalDialog from './components/ApprovalDialog.vue'
|
||||
import ExecuteDialog from './components/ExecuteDialog.vue'
|
||||
import InvalidDialog from './components/InvalidDialog.vue'
|
||||
@@ -586,7 +561,9 @@ import {
|
||||
getCrmAttachmentCategoryLabel,
|
||||
isCrmImageFile,
|
||||
resolveCrmAttachmentFiles
|
||||
} from '@/utils/crm-attachment';
|
||||
} from '@/utils/crm-attachment'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
|
||||
const userStore = useUserStore();
|
||||
const exportLoading = ref(false);
|
||||
@@ -615,135 +592,64 @@ const formDialogEditId = ref<string | null>(null)
|
||||
|
||||
const { loadPerms, hasContractPerm } = useContractPerm()
|
||||
|
||||
// 列配置相关
|
||||
const COLUMN_STORAGE_KEY = 'crm_contract_visible_columns';
|
||||
// 列配置
|
||||
const listColumnDefs = [
|
||||
{ prop: 'customerName', label: '客户名称' },
|
||||
{ prop: 'contactName', label: '联系人' },
|
||||
{ prop: 'contactMobile', label: '联系人手机' },
|
||||
{ prop: 'contractType', label: '合同类型' },
|
||||
{ prop: 'renewalCount', label: '续签次数' },
|
||||
{ prop: 'attachmentFiles', label: '合同附件' },
|
||||
{ prop: 'amount', label: '合同金额(元)' },
|
||||
{ prop: 'receivedAmount', label: '已回款' },
|
||||
{ prop: 'unreceivedAmount', label: '未回款' },
|
||||
{ prop: 'receivedRate', label: '回款进度' },
|
||||
{ prop: 'invoiceAmount', label: '已开票金额' },
|
||||
{ prop: 'uninvoiceAmount', label: '未开票金额' },
|
||||
{ prop: 'status', label: '合同状态' },
|
||||
{ prop: 'approvalStatus', label: '审批状态' },
|
||||
{ prop: 'signDate', label: '签订日期' },
|
||||
{ prop: 'startDate', label: '开始日期' },
|
||||
{ prop: 'endDate', label: '结束日期' },
|
||||
{ prop: 'paymentMethod', label: '付款方式' },
|
||||
{ prop: 'partyA', label: '甲方' },
|
||||
{ prop: 'partyB', label: '乙方' },
|
||||
{ prop: 'settlementMethod', label: '结算方式' },
|
||||
{ prop: 'ownerUserName', label: '负责人' },
|
||||
{ prop: 'createDeptName', label: '创建部门' },
|
||||
{ prop: 'createByName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'updateByName', label: '更新人' },
|
||||
{ prop: 'updateTime', label: '更新时间' },
|
||||
{ prop: 'remark', label: '备注' }
|
||||
]
|
||||
|
||||
// 定义所有列配置(顺序代表显示顺序)
|
||||
const tableColumns = ref([
|
||||
{ prop: 'customerName', label: '客户名称', fixed: false },
|
||||
{ prop: 'contactName', label: '联系人', fixed: false },
|
||||
{ prop: 'contactMobile', label: '联系人手机', fixed: false },
|
||||
{ prop: 'contractType', label: '合同类型', fixed: false },
|
||||
{ prop: 'renewalCount', label: '续签次数', fixed: false },
|
||||
{ prop: 'attachmentFiles', label: '合同附件', fixed: false },
|
||||
{ prop: 'amount', label: '合同金额(元)', fixed: false },
|
||||
{ prop: 'receivedAmount', label: '已回款', fixed: false },
|
||||
{ prop: 'unreceivedAmount', label: '未回款', fixed: false },
|
||||
{ prop: 'receivedRate', label: '回款进度', fixed: false },
|
||||
{ prop: 'invoiceAmount', label: '已开票金额', fixed: false },
|
||||
{ prop: 'uninvoiceAmount', label: '未开票金额', fixed: false },
|
||||
{ prop: 'status', label: '合同状态', fixed: false },
|
||||
{ prop: 'approvalStatus', label: '审批状态', fixed: false },
|
||||
{ prop: 'signDate', label: '签订日期', fixed: false },
|
||||
{ prop: 'startDate', label: '开始日期', fixed: false },
|
||||
{ prop: 'endDate', label: '结束日期', fixed: false },
|
||||
{ prop: 'paymentMethod', label: '付款方式', fixed: false },
|
||||
{ prop: 'partyA', label: '甲方', fixed: false },
|
||||
{ prop: 'partyB', label: '乙方', fixed: false },
|
||||
{ prop: 'settlementMethod', label: '结算方式', fixed: false },
|
||||
{ prop: 'ownerUserName', label: '负责人', fixed: false },
|
||||
{ prop: 'createDeptName', label: '创建部门', fixed: false },
|
||||
{ prop: 'createByName', label: '创建人', fixed: false },
|
||||
{ prop: 'createTime', label: '创建时间', fixed: false },
|
||||
{ prop: 'updateByName', label: '更新人', fixed: false },
|
||||
{ prop: 'updateTime', label: '更新时间', fixed: false },
|
||||
{ prop: 'remark', label: '备注', fixed: false }
|
||||
]);
|
||||
|
||||
// 默认显示的列(现有字段)
|
||||
const defaultVisibleColumns = [
|
||||
'customerName',
|
||||
'contractType',
|
||||
'attachmentFiles',
|
||||
'amount',
|
||||
'receivedAmount',
|
||||
'receivedRate',
|
||||
'status',
|
||||
'approvalStatus',
|
||||
'ownerUserName',
|
||||
'signDate',
|
||||
'createTime'
|
||||
];
|
||||
|
||||
// 当前显示的列
|
||||
const visibleColumns = ref<string[]>([]);
|
||||
|
||||
// 列设置对话框
|
||||
const columnSettingDialog = reactive({
|
||||
visible: false
|
||||
});
|
||||
|
||||
// 全选状态
|
||||
const checkAll = ref(false);
|
||||
const isIndeterminate = ref(false);
|
||||
|
||||
/** 初始化列配置 */
|
||||
const initColumnSetting = () => {
|
||||
const savedColumns = localStorage.getItem(COLUMN_STORAGE_KEY);
|
||||
if (savedColumns) {
|
||||
try {
|
||||
visibleColumns.value = JSON.parse(savedColumns);
|
||||
} catch (error) {
|
||||
console.error('解析列配置失败:', error);
|
||||
visibleColumns.value = [...defaultVisibleColumns];
|
||||
}
|
||||
} else {
|
||||
visibleColumns.value = [...defaultVisibleColumns];
|
||||
}
|
||||
updateCheckAllStatus();
|
||||
};
|
||||
|
||||
/** 判断列是否显示 */
|
||||
const isColumnVisible = (prop: string) => {
|
||||
return visibleColumns.value.includes(prop);
|
||||
};
|
||||
|
||||
/** 打开列设置对话框 */
|
||||
const handleColumnSetting = () => {
|
||||
columnSettingDialog.visible = true;
|
||||
};
|
||||
|
||||
/** 全选/取消全选 */
|
||||
const handleCheckAllChange = (val: boolean) => {
|
||||
visibleColumns.value = val ? tableColumns.value.map((col) => col.prop) : [];
|
||||
isIndeterminate.value = false;
|
||||
};
|
||||
|
||||
/** 列选择变化 */
|
||||
const handleCheckedColumnsChange = () => {
|
||||
updateCheckAllStatus();
|
||||
};
|
||||
|
||||
/** 更新全选状态 */
|
||||
const updateCheckAllStatus = () => {
|
||||
const checkedCount = visibleColumns.value.length;
|
||||
const totalCount = tableColumns.value.length;
|
||||
checkAll.value = checkedCount === totalCount;
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < totalCount;
|
||||
};
|
||||
|
||||
/** 恢复默认列设置 */
|
||||
const handleResetColumns = () => {
|
||||
visibleColumns.value = [...defaultVisibleColumns];
|
||||
updateCheckAllStatus();
|
||||
};
|
||||
|
||||
/** 保存列设置 */
|
||||
const handleSaveColumnSetting = () => {
|
||||
try {
|
||||
localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(visibleColumns.value));
|
||||
$modal.msgSuccess('列设置保存成功');
|
||||
columnSettingDialog.visible = false;
|
||||
} catch (error) {
|
||||
console.error('保存列配置失败:', error);
|
||||
$modal.msgError('保存列设置失败');
|
||||
}
|
||||
};
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('crm_contract_visible_columns', listColumnDefs, {
|
||||
defaultVisibleProps: [
|
||||
'customerName',
|
||||
'contractType',
|
||||
'attachmentFiles',
|
||||
'amount',
|
||||
'receivedAmount',
|
||||
'receivedRate',
|
||||
'status',
|
||||
'approvalStatus',
|
||||
'ownerUserName',
|
||||
'signDate',
|
||||
'createTime'
|
||||
]
|
||||
})
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref<ContractQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
contractNo: undefined,
|
||||
contractName: undefined,
|
||||
customerName: undefined,
|
||||
@@ -1340,7 +1246,6 @@ watchDebounced(
|
||||
|
||||
onMounted(async () => {
|
||||
await loadPerms()
|
||||
initColumnSetting();
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
<!-- 合同模板管理 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 查询表单 -->
|
||||
<el-card shadow="hover" v-show="showSearch" class="mb-[10px]">
|
||||
<el-form :model="queryParams" ref="queryFormRef" label-width="90px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">合同模板</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form ref="queryFormRef" :model="queryParams" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="模板名称" prop="templateName">
|
||||
<el-input v-model="queryParams.templateName" placeholder="请输入模板名称" clearable @keyup.enter="handleQuery" />
|
||||
<el-input
|
||||
v-model="queryParams.templateName"
|
||||
placeholder="模板名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<el-form-item label="模板类型" prop="templateType">
|
||||
<el-select v-model="queryParams.templateType" placeholder="请选择模板类型" clearable style="width: 100%">
|
||||
<el-select v-model="queryParams.templateType" placeholder="全部" clearable>
|
||||
<el-option
|
||||
v-for="dict in dictStore.getOptions('crm_contract_type')"
|
||||
:key="dict.value"
|
||||
@@ -20,99 +27,180 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<el-form-item label="状态" prop="isActive">
|
||||
<el-select v-model="queryParams.isActive" placeholder="请选择状态" clearable style="width: 100%">
|
||||
<el-select v-model="queryParams.isActive" placeholder="全部" clearable>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="停用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24" style="text-align: right">
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按模板名称、类型筛选。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="resetQuery">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col v-if="hasContractPerm('templateAdd')" :span="1.5">
|
||||
<el-button type="primary" icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('templateRemove')" :span="1.5">
|
||||
<el-button type="danger" icon="Delete" :disabled="multiple || !allRowsDeletableByCreator(templateList.filter((item) => ids.includes(item.id)))" :title="allRowsDeletableByCreator(templateList.filter((item) => ids.includes(item.id))) ? '' : CREATOR_DELETE_TIP" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="hasContractPerm('templateExport')" :span="1.5">
|
||||
<el-button type="success" icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
|
||||
</el-row>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table v-loading="loading" :data="templateList" @selection-change="handleSelectionChange" border>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="模板名称" prop="templateName" :show-overflow-tooltip="true" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-link type="primary" @click="handleView(scope.row)">{{ scope.row.templateName }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模板类型" align="center" prop="templateType" width="120">
|
||||
<template #default="scope">
|
||||
<DictTag dict-type="crm_contract_type" :value="scope.row.templateType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模板编号" align="center" prop="templateNo" width="150" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="状态" align="center" prop="isActive" width="100">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button
|
||||
v-if="hasContractPerm('templateAdd')"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>新增模板
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('templateEdit')"
|
||||
v-model="scope.row.isActive"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleStatusChange(scope.row)"
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('templateRemove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="
|
||||
selectedRows.length === 0 ||
|
||||
!allRowsDeletableByCreator(selectedRows)
|
||||
"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('templateExport')"
|
||||
class="btn-ghost"
|
||||
:loading="exportLoading"
|
||||
@click="handleExport"
|
||||
>
|
||||
<el-icon><Download /></el-icon>导出
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions :loading="loading" @refresh="getList" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
v-loading="loading"
|
||||
:data="templateList"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
prop="templateName"
|
||||
label="模板名称"
|
||||
min-width="200"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<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">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="dictStore.getTagType('crm_contract_type', row.templateType)"
|
||||
size="small"
|
||||
>
|
||||
{{ dictStore.getLabel('crm_contract_type', row.templateType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="templateNo"
|
||||
label="模板编号"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-tag v-else :type="scope.row.isActive === 1 ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.isActive === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用次数" align="center" prop="usageCount" width="100" />
|
||||
<el-table-column label="排序" align="center" prop="sort" width="80" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="160">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-if="hasContractPerm('templateEdit')" content="编辑" placement="top">
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="hasContractPerm('templateRemove') && canDeleteByCreator(scope.row)" content="删除" placement="top">
|
||||
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table-column label="状态" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-if="hasContractPerm('templateEdit')"
|
||||
v-model="row.isActive"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleStatusChange(row)"
|
||||
/>
|
||||
<el-tag v-else :type="row.isActive === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.isActive === 1 ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="usageCount"
|
||||
label="使用次数"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column prop="sort" label="排序" min-width="80" align="center" header-align="center" />
|
||||
<el-table-column label="创建时间" min-width="160" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ parseTime(row.createTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="queryParams.pageNum"
|
||||
v-model:page-size="queryParams.pageSize"
|
||||
:page-sizes="[10, 20, 30, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="getList"
|
||||
@current-change="getList"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加或修改对话框 -->
|
||||
<el-dialog :title="dialog.title" v-model="dialog.visible" width="900px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-dialog
|
||||
v-model="dialog.visible"
|
||||
:title="dialog.title"
|
||||
width="900px"
|
||||
class="dialog-form-layout order-module-dialog-mask"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
@close="cancel"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="模板名称" prop="templateName">
|
||||
<el-input v-model="form.templateName" placeholder="请输入模板名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="模板类型" prop="templateType">
|
||||
<el-select v-model="form.templateType" placeholder="请选择模板类型" style="width: 100%">
|
||||
<el-option
|
||||
@@ -123,63 +211,98 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="isActive">
|
||||
<el-radio-group v-model="form.isActive">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="9999" controls-position="right" style="width: 100%" />
|
||||
<el-input-number
|
||||
v-model="form.sort"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="合同条款" prop="contractTerms">
|
||||
<el-input v-model="form.contractTerms" type="textarea" :rows="4" placeholder="请输入合同条款" />
|
||||
</el-form-item>
|
||||
<el-form-item label="付款条款" prop="paymentTerms">
|
||||
<el-input v-model="form.paymentTerms" type="textarea" :rows="3" placeholder="请输入付款条款" />
|
||||
</el-form-item>
|
||||
<el-form-item label="交付条款" prop="deliveryTerms">
|
||||
<el-input v-model="form.deliveryTerms" type="textarea" :rows="3" placeholder="请输入交付条款" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板内容" prop="templateContent">
|
||||
<el-input v-model="form.templateContent" type="textarea" :rows="6" placeholder="请输入模板内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-form-item label="合同条款" prop="contractTerms" class="form-col-span-2">
|
||||
<el-input v-model="form.contractTerms" type="textarea" :rows="4" placeholder="请输入合同条款" />
|
||||
</el-form-item>
|
||||
<el-form-item label="付款条款" prop="paymentTerms" class="form-col-span-2">
|
||||
<el-input v-model="form.paymentTerms" type="textarea" :rows="3" placeholder="请输入付款条款" />
|
||||
</el-form-item>
|
||||
<el-form-item label="交付条款" prop="deliveryTerms" class="form-col-span-2">
|
||||
<el-input v-model="form.deliveryTerms" type="textarea" :rows="3" placeholder="请输入交付条款" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模板内容" prop="templateContent" class="form-col-span-2">
|
||||
<el-input v-model="form.templateContent" type="textarea" :rows="6" placeholder="请输入模板内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark" class="form-col-span-2">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="cancel">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="submitLoading" @click="submitForm">
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 查看详情 -->
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewDialog.visible"
|
||||
:title="viewDialog.data.templateName || '模板详情'"
|
||||
:icon="Document"
|
||||
:code="viewDialog.data.templateNo"
|
||||
code-label="模板编号"
|
||||
:sections="templateViewSections"
|
||||
append-to-body
|
||||
/>
|
||||
<SystemViewDialog
|
||||
v-model:visible="viewDialog.visible"
|
||||
:title="viewDialog.data.templateName || '模板详情'"
|
||||
:icon="Document"
|
||||
:code="viewDialog.data.templateNo"
|
||||
code-label="模板编号"
|
||||
:sections="templateViewSections"
|
||||
append-to-body
|
||||
>
|
||||
<template #hero-actions>
|
||||
<div class="template-view-hero-actions">
|
||||
<el-button
|
||||
v-if="hasContractPerm('templateEdit')"
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
@click="handleDetailEdit"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasContractPerm('templateRemove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(viewDialog.data)"
|
||||
:title="canDeleteByCreator(viewDialog.data) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDetailDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</SystemViewDialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="ContractTemplate">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Document,
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Download,
|
||||
Search,
|
||||
RefreshLeft,
|
||||
Close,
|
||||
Check
|
||||
} from '@element-plus/icons-vue'
|
||||
import SystemViewDialog, {
|
||||
type ViewField,
|
||||
type ViewSection
|
||||
@@ -200,25 +323,27 @@ import {
|
||||
} from '@/api/crm/contract/template'
|
||||
import { parseTime } from '@/utils/format'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import DictTag from '@/components/DictTag/index.vue'
|
||||
import { $modal } from '@/utils/modal'
|
||||
import { useContractPerm } from '@/hooks/business/useContractPerm'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
canDeleteByCreator,
|
||||
filterDeletableByCreator,
|
||||
allRowsDeletableByCreator,
|
||||
CREATOR_DELETE_TIP
|
||||
} from '@/utils/creator-delete'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const { loadPerms, hasContractPerm } = useContractPerm()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref<TemplateQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
templateNo: undefined,
|
||||
templateName: undefined,
|
||||
templateType: undefined,
|
||||
isActive: undefined
|
||||
});
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const form = ref<TemplateForm>({
|
||||
templateName: '',
|
||||
templateType: '',
|
||||
@@ -229,35 +354,31 @@ const form = ref<TemplateForm>({
|
||||
isActive: 1,
|
||||
sort: 0,
|
||||
remark: ''
|
||||
});
|
||||
})
|
||||
|
||||
// 表单校验规则
|
||||
const rules = ref({
|
||||
templateName: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }],
|
||||
templateType: [{ required: true, message: '模板类型不能为空', trigger: 'change' }],
|
||||
templateContent: [{ required: true, message: '模板内容不能为空', trigger: 'blur' }]
|
||||
});
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const templateList = ref<TemplateVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const total = ref(0);
|
||||
const showSearch = ref(false);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const ids = ref<number[]>([]);
|
||||
const templateList = ref<TemplateVO[]>([])
|
||||
const selectedRows = ref<TemplateVO[]>([])
|
||||
const loading = ref(true)
|
||||
const submitLoading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const total = ref(0)
|
||||
const ids = ref<number[]>([])
|
||||
|
||||
// 对话框
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
})
|
||||
|
||||
// 查看详情对话框
|
||||
const viewDialog = reactive({
|
||||
visible: false,
|
||||
data: {} as TemplateVO
|
||||
});
|
||||
})
|
||||
|
||||
const templateViewSections = computed<ViewSection[]>(() => {
|
||||
const d = viewDialog.data
|
||||
@@ -286,87 +407,99 @@ const templateViewSections = computed<ViewSection[]>(() => {
|
||||
return sections
|
||||
})
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const formRef = ref<ElFormInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>()
|
||||
const formRef = ref<ElFormInstance>()
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await listTemplate(queryParams.value);
|
||||
templateList.value = res.rows;
|
||||
total.value = res.total;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
$modal.msgWarning('请选择一条数据进行编辑')
|
||||
return
|
||||
}
|
||||
};
|
||||
handleUpdate(selectedRows.value[0])
|
||||
}
|
||||
|
||||
async function handleDetailEdit() {
|
||||
if (!viewDialog.data?.id) return
|
||||
viewDialog.visible = false
|
||||
await handleUpdate(viewDialog.data as TemplateVO)
|
||||
}
|
||||
|
||||
async function handleDetailDelete() {
|
||||
if (!viewDialog.data?.id) return
|
||||
await handleDelete(viewDialog.data as TemplateVO)
|
||||
viewDialog.visible = false
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listTemplate(queryParams.value)
|
||||
templateList.value = res.rows
|
||||
total.value = res.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
queryParams.value.pageNum = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
queryFormRef.value?.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: TemplateVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
single.value = selection.length !== 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
selectedRows.value = selection
|
||||
ids.value = selection.map((item) => item.id)
|
||||
}
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加合同模板';
|
||||
};
|
||||
reset()
|
||||
dialog.visible = true
|
||||
dialog.title = '添加合同模板'
|
||||
}
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row: TemplateVO) => {
|
||||
reset();
|
||||
const res = await getTemplate(row.id);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改合同模板';
|
||||
};
|
||||
reset()
|
||||
const res = await getTemplate(row.id)
|
||||
Object.assign(form.value, res.data)
|
||||
dialog.visible = true
|
||||
dialog.title = '修改合同模板'
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
const handleView = async (row: TemplateVO) => {
|
||||
const res = await getTemplate(row.id);
|
||||
viewDialog.data = res.data;
|
||||
viewDialog.visible = true;
|
||||
};
|
||||
const res = await getTemplate(row.id)
|
||||
viewDialog.data = res.data
|
||||
viewDialog.visible = true
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
const submitForm = () => {
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (form.value.id) {
|
||||
await updateTemplate(form.value);
|
||||
$modal.msgSuccess('修改成功')
|
||||
} else {
|
||||
await addTemplate(form.value)
|
||||
$modal.msgSuccess('新增成功')
|
||||
}
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (!valid) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (form.value.id) {
|
||||
await updateTemplate(form.value)
|
||||
$modal.msgSuccess('修改成功')
|
||||
} else {
|
||||
await addTemplate(form.value)
|
||||
$modal.msgSuccess('新增成功')
|
||||
}
|
||||
dialog.visible = false
|
||||
await getList()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: TemplateVO) => {
|
||||
const targets = row ? [row] : templateList.value.filter((item) => ids.value.includes(item.id))
|
||||
const targets = row ? [row] : selectedRows.value
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
@@ -376,17 +509,16 @@ const handleDelete = async (row?: TemplateVO) => {
|
||||
$modal.msgWarning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
const idList = row?.id ? [row.id] : ids.value;
|
||||
const idList = row?.id ? [row.id] : ids.value
|
||||
await $modal.confirm('是否确认删除选中的合同模板?')
|
||||
await delTemplate(idList)
|
||||
$modal.msgSuccess('删除成功')
|
||||
await getList();
|
||||
};
|
||||
await getList()
|
||||
}
|
||||
|
||||
/** 状态切换 */
|
||||
const handleStatusChange = async (row: TemplateVO) => {
|
||||
try {
|
||||
const text = row.isActive === 1 ? '启用' : '停用';
|
||||
const text = row.isActive === 1 ? '启用' : '停用'
|
||||
await $modal.confirm(`确认要${text}"${row.templateName}"吗?`)
|
||||
if (row.isActive === 1) {
|
||||
await enableTemplate(row.id)
|
||||
@@ -394,14 +526,14 @@ const handleStatusChange = async (row: TemplateVO) => {
|
||||
await disableTemplate(row.id)
|
||||
}
|
||||
$modal.msgSuccess(`${text}成功`)
|
||||
await getList();
|
||||
await getList()
|
||||
} catch {
|
||||
row.isActive = row.isActive === 0 ? 1 : 0;
|
||||
row.isActive = row.isActive === 0 ? 1 : 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = async () => {
|
||||
exportLoading.value = true
|
||||
try {
|
||||
const res = await exportTemplate(queryParams.value)
|
||||
const blob =
|
||||
@@ -419,16 +551,16 @@ const handleExport = async () => {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
$modal.msgError('导出失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
dialog.visible = false;
|
||||
reset();
|
||||
};
|
||||
dialog.visible = false
|
||||
reset()
|
||||
}
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = {
|
||||
id: undefined,
|
||||
@@ -441,19 +573,12 @@ const reset = () => {
|
||||
isActive: 1,
|
||||
sort: 0,
|
||||
remark: ''
|
||||
};
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadPerms()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.text-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -235,7 +235,13 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleViewOpportunity(row.id)">
|
||||
{{ row.opportunityName }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商机阶段" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
@@ -267,20 +273,6 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
fixed="right"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
class-name="col-actions"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleViewOpportunity(row.id)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -310,7 +302,13 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleViewContract(row.id)">
|
||||
{{ row.contractName }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="合同类型" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
@@ -348,20 +346,6 @@
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
fixed="right"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
class-name="col-actions"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleViewContract(row.id)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -523,7 +507,7 @@ const followTotal = ref(0)
|
||||
const followQuery = ref<CustomerFollowQuery>({
|
||||
customerId: undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
const contactList = ref<ContactVO[]>([])
|
||||
@@ -536,7 +520,7 @@ const operLogList = ref<OperLogVO[]>([])
|
||||
const operLogListFull = ref<OperLogVO[]>([])
|
||||
const logTotal = ref(0)
|
||||
const logLoading = ref(false)
|
||||
const logQuery = ref({ pageNum: 1, pageSize: 10 })
|
||||
const logQuery = ref({ pageNum: 1, pageSize: 20 })
|
||||
const activeTab = ref('follow')
|
||||
|
||||
const customerFormRef = ref<InstanceType<typeof CustomerForm>>()
|
||||
@@ -910,8 +894,8 @@ function open(id: number | string) {
|
||||
operLogList.value = []
|
||||
operLogListFull.value = []
|
||||
activeTab.value = 'follow'
|
||||
followQuery.value = { customerId: id, pageNum: 1, pageSize: 10 }
|
||||
logQuery.value = { pageNum: 1, pageSize: 10 }
|
||||
followQuery.value = { customerId: id, pageNum: 1, pageSize: 20 }
|
||||
logQuery.value = { pageNum: 1, pageSize: 20 }
|
||||
getCustomerInfo()
|
||||
getFollowList()
|
||||
}
|
||||
|
||||
@@ -3,17 +3,19 @@
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2>客户列表</h2>
|
||||
<h2 class="title-bar-heading">客户列表</h2>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs">
|
||||
<el-tabs v-model="activeView" @tab-change="handleViewChange">
|
||||
<el-tab-pane label="我的客户" name="mine" />
|
||||
<el-tab-pane label="全部客户" name="all" />
|
||||
<el-tab-pane label="待跟进" name="toFollow" />
|
||||
<el-tab-pane label="重点客户" name="key" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-tabs
|
||||
v-model="activeView"
|
||||
class="panel-pill-tabs panel-pill-tabs--lg"
|
||||
@tab-change="handleViewChange"
|
||||
>
|
||||
<el-tab-pane label="我的客户" name="mine" />
|
||||
<el-tab-pane label="全部客户" name="all" />
|
||||
<el-tab-pane label="待跟进" name="toFollow" />
|
||||
<el-tab-pane label="重点客户" name="key" />
|
||||
</el-tabs>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
<div class="filter-block">
|
||||
@@ -162,10 +164,38 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增客户
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :disabled="multiple" @click="handleIntoPool()">
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
放入公海
|
||||
</el-button>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -178,6 +208,7 @@
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -591,33 +622,6 @@
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="320"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
class-name="col-actions"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="col-actions-inner">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item btn-op-danger"
|
||||
:disabled="!canDeleteByCreator(row)"
|
||||
:title="canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
@@ -649,46 +653,6 @@
|
||||
|
||||
<!-- 客户成交对话框 -->
|
||||
<deal-form ref="dealFormRef" @success="getList" />
|
||||
|
||||
<!-- 列设置对话框 -->
|
||||
<el-dialog
|
||||
v-model="columnSettingDialog.visible"
|
||||
title="列设置"
|
||||
width="600px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="column-setting-content">
|
||||
<el-checkbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAllChange"
|
||||
>
|
||||
全选
|
||||
</el-checkbox>
|
||||
<el-divider />
|
||||
<el-checkbox-group v-model="visibleColumns" @change="handleCheckedColumnsChange">
|
||||
<div v-for="col in tableColumns" :key="col.prop" class="column-item">
|
||||
<el-checkbox :label="col.prop" :disabled="col.fixed">{{ col.label }}</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button class="btn-ghost" @click="columnSettingDialog.visible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handleResetColumns">
|
||||
<el-icon><RefreshLeft /></el-icon>恢复默认
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSaveColumnSetting">
|
||||
<el-icon><Check /></el-icon>保存
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -697,18 +661,20 @@
|
||||
import {
|
||||
Plus,
|
||||
Delete,
|
||||
Close,
|
||||
Check,
|
||||
Search,
|
||||
RefreshLeft,
|
||||
View,
|
||||
DocumentCopy,
|
||||
ArrowUpBold,
|
||||
ArrowDownBold,
|
||||
Edit,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
canDeleteByCreator,
|
||||
filterDeletableByCreator,
|
||||
allRowsDeletableByCreator,
|
||||
CREATOR_DELETE_TIP
|
||||
} from '@/utils/creator-delete'
|
||||
import {
|
||||
fetchCustomerList,
|
||||
fetchCustomerDetail,
|
||||
@@ -729,6 +695,8 @@ import TransferForm from './TransferDialog.vue'
|
||||
import IntoPoolForm from './IntoPoolDialog.vue'
|
||||
import FollowForm from './FollowFormDialog.vue'
|
||||
import DealForm from './DealDialog.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
@@ -755,75 +723,76 @@ const activeView = ref('mine')
|
||||
const customerList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
// 多选状态
|
||||
const single = ref(true)
|
||||
const multiple = ref(true)
|
||||
const ids = ref<(number | string)[]>([])
|
||||
|
||||
// 列配置相关
|
||||
const COLUMN_STORAGE_KEY = 'crm_customer_visible_columns'
|
||||
|
||||
const tableColumns = ref([
|
||||
{ prop: 'shortName', label: '客户简称', fixed: false },
|
||||
{ prop: 'customerType', label: '客户类型', fixed: false },
|
||||
{ prop: 'mobile', label: '手机号', fixed: false },
|
||||
{ prop: 'telephone', label: '固定电话', fixed: false },
|
||||
{ prop: 'email', label: '邮箱', fixed: false },
|
||||
{ prop: 'website', label: '客户网站', fixed: false },
|
||||
{ prop: 'level', label: '客户级别', fixed: false },
|
||||
{ prop: 'source', label: '客户来源', fixed: false },
|
||||
{ prop: 'industry', label: '所属行业', fixed: false },
|
||||
{ prop: 'parkArea', label: '所在园区', fixed: false },
|
||||
{ prop: 'scale', label: '企业规模', fixed: false },
|
||||
{ prop: 'province', label: '省', fixed: false },
|
||||
{ prop: 'city', label: '市', fixed: false },
|
||||
{ prop: 'district', label: '区', fixed: false },
|
||||
{ prop: 'address', label: '详细地址', fixed: false },
|
||||
{ prop: 'status', label: '客户状态', fixed: false },
|
||||
{ prop: 'dealStatus', label: '成交状态', fixed: false },
|
||||
{ prop: 'ownerUserName', label: '负责人', fixed: false },
|
||||
{ prop: 'followCount', label: '跟进次数', fixed: false },
|
||||
{ prop: 'lastTime', label: '最后跟进时间', fixed: false },
|
||||
{ prop: 'nextTime', label: '下次联系时间', fixed: false },
|
||||
{ prop: 'dealTime', label: '成交时间', fixed: false },
|
||||
{ prop: 'dealAmount', label: '成交金额', fixed: false },
|
||||
{ prop: 'contactCount', label: '联系人数量', fixed: false },
|
||||
{ prop: 'opportunityCount', label: '商机数量', fixed: false },
|
||||
{ prop: 'contractCount', label: '合同数量', fixed: false },
|
||||
{ prop: 'intoPoolTime', label: '进入公海时间', fixed: false },
|
||||
{ prop: 'claimTime', label: '领取时间', fixed: false },
|
||||
{ prop: 'protectTime', label: '保护到期时间', fixed: false },
|
||||
{ prop: 'createDeptName', label: '创建部门', fixed: false },
|
||||
{ prop: 'createByName', label: '创建人', fixed: false },
|
||||
{ prop: 'createTime', label: '创建时间', fixed: false },
|
||||
{ prop: 'updateByName', label: '更新人', fixed: false },
|
||||
{ prop: 'updateTime', label: '更新时间', fixed: false },
|
||||
{ prop: 'remark', label: '备注', fixed: false },
|
||||
])
|
||||
|
||||
const defaultVisibleColumns = [
|
||||
'customerType',
|
||||
'mobile',
|
||||
'level',
|
||||
'source',
|
||||
'industry',
|
||||
'parkArea',
|
||||
'status',
|
||||
'dealStatus',
|
||||
'ownerUserName',
|
||||
'followCount',
|
||||
'dealAmount',
|
||||
'lastTime',
|
||||
'nextTime',
|
||||
'createTime',
|
||||
// 列配置
|
||||
const listColumnDefs = [
|
||||
{ prop: 'shortName', label: '客户简称' },
|
||||
{ prop: 'customerType', label: '客户类型' },
|
||||
{ prop: 'mobile', label: '手机号' },
|
||||
{ prop: 'telephone', label: '固定电话' },
|
||||
{ prop: 'email', label: '邮箱' },
|
||||
{ prop: 'website', label: '客户网站' },
|
||||
{ prop: 'level', label: '客户级别' },
|
||||
{ prop: 'source', label: '客户来源' },
|
||||
{ prop: 'industry', label: '所属行业' },
|
||||
{ prop: 'parkArea', label: '所在园区' },
|
||||
{ prop: 'scale', label: '企业规模' },
|
||||
{ prop: 'province', label: '省' },
|
||||
{ prop: 'city', label: '市' },
|
||||
{ prop: 'district', label: '区' },
|
||||
{ prop: 'address', label: '详细地址' },
|
||||
{ prop: 'status', label: '客户状态' },
|
||||
{ prop: 'dealStatus', label: '成交状态' },
|
||||
{ prop: 'ownerUserName', label: '负责人' },
|
||||
{ prop: 'followCount', label: '跟进次数' },
|
||||
{ prop: 'lastTime', label: '最后跟进时间' },
|
||||
{ prop: 'nextTime', label: '下次联系时间' },
|
||||
{ prop: 'dealTime', label: '成交时间' },
|
||||
{ prop: 'dealAmount', label: '成交金额' },
|
||||
{ prop: 'contactCount', label: '联系人数量' },
|
||||
{ prop: 'opportunityCount', label: '商机数量' },
|
||||
{ prop: 'contractCount', label: '合同数量' },
|
||||
{ prop: 'intoPoolTime', label: '进入公海时间' },
|
||||
{ prop: 'claimTime', label: '领取时间' },
|
||||
{ prop: 'protectTime', label: '保护到期时间' },
|
||||
{ prop: 'createDeptName', label: '创建部门' },
|
||||
{ prop: 'createByName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'updateByName', label: '更新人' },
|
||||
{ prop: 'updateTime', label: '更新时间' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
]
|
||||
|
||||
const visibleColumns = ref<string[]>([])
|
||||
const columnSettingDialog = reactive({ visible: false })
|
||||
const checkAll = ref(false)
|
||||
const isIndeterminate = ref(false)
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('crm_customer_visible_columns', listColumnDefs, {
|
||||
defaultVisibleProps: [
|
||||
'customerType',
|
||||
'mobile',
|
||||
'level',
|
||||
'source',
|
||||
'industry',
|
||||
'parkArea',
|
||||
'status',
|
||||
'dealStatus',
|
||||
'ownerUserName',
|
||||
'followCount',
|
||||
'dealAmount',
|
||||
'lastTime',
|
||||
'nextTime',
|
||||
'createTime',
|
||||
]
|
||||
})
|
||||
|
||||
// 子组件引用
|
||||
const customerFormRef = ref<InstanceType<typeof CustomerForm>>()
|
||||
@@ -905,6 +874,14 @@ function handleViewChange() {
|
||||
getList()
|
||||
}
|
||||
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请选择一条数据进行编辑')
|
||||
return
|
||||
}
|
||||
handleEdit(selectedRows.value[0])
|
||||
}
|
||||
|
||||
/** 编辑客户 */
|
||||
function handleEdit(row: any) {
|
||||
customerFormRef.value?.open(row.id)
|
||||
@@ -976,8 +953,7 @@ function handleDeal(row?: any) {
|
||||
|
||||
/** 更多操作命令 */
|
||||
function handleMoreCommand(cmd: string, row: any) {
|
||||
if (cmd === 'view') handleView(row)
|
||||
else if (cmd === 'edit') handleEdit(row)
|
||||
if (cmd === 'edit') handleEdit(row)
|
||||
else if (cmd === 'transfer') handleTransfer(row)
|
||||
else if (cmd === 'intoPool') handleIntoPool(row)
|
||||
else if (cmd === 'delete') handleDelete(row)
|
||||
@@ -1029,61 +1005,6 @@ async function handleExport() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 列设置 ──
|
||||
|
||||
function initColumnSetting() {
|
||||
const savedColumns = localStorage.getItem(COLUMN_STORAGE_KEY)
|
||||
if (savedColumns) {
|
||||
try {
|
||||
visibleColumns.value = JSON.parse(savedColumns)
|
||||
} catch {
|
||||
visibleColumns.value = [...defaultVisibleColumns]
|
||||
}
|
||||
} else {
|
||||
visibleColumns.value = [...defaultVisibleColumns]
|
||||
}
|
||||
updateCheckAllStatus()
|
||||
}
|
||||
|
||||
function isColumnVisible(prop: string) {
|
||||
return visibleColumns.value.includes(prop)
|
||||
}
|
||||
|
||||
function handleColumnSetting() {
|
||||
columnSettingDialog.visible = true
|
||||
}
|
||||
|
||||
function handleCheckAllChange(val: boolean) {
|
||||
visibleColumns.value = val ? tableColumns.value.map((col) => col.prop) : []
|
||||
isIndeterminate.value = false
|
||||
}
|
||||
|
||||
function handleCheckedColumnsChange() {
|
||||
updateCheckAllStatus()
|
||||
}
|
||||
|
||||
function updateCheckAllStatus() {
|
||||
const checkedCount = visibleColumns.value.length
|
||||
const totalCount = tableColumns.value.length
|
||||
checkAll.value = checkedCount === totalCount
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < totalCount
|
||||
}
|
||||
|
||||
function handleResetColumns() {
|
||||
visibleColumns.value = [...defaultVisibleColumns]
|
||||
updateCheckAllStatus()
|
||||
}
|
||||
|
||||
function handleSaveColumnSetting() {
|
||||
try {
|
||||
localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(visibleColumns.value))
|
||||
ElMessage.success('列设置保存成功')
|
||||
columnSettingDialog.visible = false
|
||||
} catch {
|
||||
ElMessage.error('保存列设置失败')
|
||||
}
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
() => ({ ...searchForm, dateRange: dateRange.value }),
|
||||
() => {
|
||||
@@ -1094,7 +1015,6 @@ watchDebounced(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initColumnSetting()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
@@ -1111,24 +1031,6 @@ onMounted(() => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.column-setting-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
.column-item {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.col-actions-inner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2>公海池规则</h2>
|
||||
<h2 class="title-bar-heading">公海池规则</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
@@ -34,10 +34,10 @@
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按规则名称模糊查询。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -56,6 +56,36 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增规则
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
v-auth="'edit'"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
v-auth="'edit'"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@click="handleToolbarToggle"
|
||||
>
|
||||
<el-icon>
|
||||
<CircleClose v-if="selectedRows[0]?.status === 'enable'" />
|
||||
<CircleCheck v-else />
|
||||
</el-icon>
|
||||
{{ selectedRows[0]?.status === 'enable' ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
v-auth="'delete'"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="warning"
|
||||
@@ -65,6 +95,8 @@
|
||||
<el-icon><VideoPlay /></el-icon>
|
||||
手动执行
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions :loading="loading" @refresh="getList" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -150,41 +182,6 @@
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="200"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item primary" v-auth="'edit'" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:class="row.status === 'enable' ? 'warning' : 'success'"
|
||||
v-auth="'edit'"
|
||||
@click="handleToggle(row)"
|
||||
>
|
||||
<el-icon>
|
||||
<CircleClose v-if="row.status === 'enable'" />
|
||||
<CircleCheck v-else />
|
||||
</el-icon>
|
||||
{{ row.status === 'enable' ? '禁用' : '启用' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item danger"
|
||||
v-auth="'delete'"
|
||||
v-if="canDeleteByCreator(row)"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
@@ -206,6 +203,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import {
|
||||
Plus,
|
||||
Close,
|
||||
@@ -238,7 +236,7 @@ const searchForm = reactive({
|
||||
const ruleList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const ruleDialogRef = ref()
|
||||
|
||||
@@ -296,6 +294,22 @@ const handleAdd = () => {
|
||||
ruleDialogRef.value?.open()
|
||||
}
|
||||
|
||||
const handleToolbarEdit = () => {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请选择一条数据进行编辑')
|
||||
return
|
||||
}
|
||||
handleEdit(selectedRows.value[0])
|
||||
}
|
||||
|
||||
const handleToolbarToggle = () => {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请选择一条数据进行操作')
|
||||
return
|
||||
}
|
||||
handleToggle(selectedRows.value[0])
|
||||
}
|
||||
|
||||
const handleEdit = (row: any) => {
|
||||
ruleDialogRef.value?.open(row)
|
||||
}
|
||||
@@ -315,12 +329,30 @@ const handleToggle = async (row: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
const handleDelete = async (row?: any) => {
|
||||
const targets = row ? [row] : selectedRows.value
|
||||
if (!targets.length) {
|
||||
ElMessage.warning('请至少选择一条数据进行删除')
|
||||
return
|
||||
}
|
||||
const deletable = filterDeletableByCreator(targets)
|
||||
if (!deletable.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
if (deletable.length < targets.length) {
|
||||
ElMessage.warning(CREATOR_DELETE_TIP)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除规则「${row.poolName}」吗?`, '提示', {
|
||||
const label =
|
||||
deletable.length === 1 ? `规则「${deletable[0].poolName}」` : `选中的 ${deletable.length} 条规则`
|
||||
await ElMessageBox.confirm(`确认删除${label}吗?`, '提示', {
|
||||
type: 'warning',
|
||||
})
|
||||
await fetchDeletePoolRule({ id: row.id })
|
||||
for (const item of deletable) {
|
||||
await fetchDeletePoolRule({ id: item.id })
|
||||
}
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
} catch {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2>公海池</h2>
|
||||
<h2 class="title-bar-heading">公海池</h2>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
@@ -60,10 +60,10 @@
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按客户名称、编号模糊查询。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -91,10 +91,16 @@
|
||||
<el-icon><User /></el-icon>
|
||||
分配客户
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handleColumnSetting">
|
||||
<el-icon><Setting /></el-icon>
|
||||
列设置
|
||||
</el-button>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -107,6 +113,7 @@
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
@@ -501,36 +508,6 @@
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="200"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item primary" @click="handleClaim(row)">
|
||||
<el-icon><Select /></el-icon>领取
|
||||
</el-button>
|
||||
<el-dropdown trigger="hover" @command="(cmd: string) => handleMoreCommand(cmd, row)">
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="view">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="assign">
|
||||
<el-icon><User /></el-icon>分配
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
@@ -553,64 +530,22 @@
|
||||
|
||||
<!-- 分配客户对话框 -->
|
||||
<assign-dialog ref="assignDialogRef" @success="getList" />
|
||||
|
||||
<!-- 列设置对话框 -->
|
||||
<el-dialog
|
||||
v-model="columnSettingDialog.visible"
|
||||
title="列设置"
|
||||
width="600px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="column-setting-content">
|
||||
<el-checkbox
|
||||
v-model="checkAll"
|
||||
:indeterminate="isIndeterminate"
|
||||
@change="handleCheckAllChange"
|
||||
>
|
||||
全选
|
||||
</el-checkbox>
|
||||
<el-divider />
|
||||
<el-checkbox-group v-model="visibleColumns" @change="handleCheckedColumnsChange">
|
||||
<div v-for="col in tableColumns" :key="col.prop" class="column-item">
|
||||
<el-checkbox :label="col.prop" :disabled="col.fixed">{{ col.label }}</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button class="btn-ghost" @click="columnSettingDialog.visible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handleResetColumns">
|
||||
<el-icon><RefreshLeft /></el-icon>恢复默认
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSaveColumnSetting">
|
||||
<el-icon><Check /></el-icon>保存
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Close,
|
||||
Check,
|
||||
Search,
|
||||
RefreshLeft,
|
||||
Select,
|
||||
User,
|
||||
Setting,
|
||||
More,
|
||||
View,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { fetchPoolCustomerList } from '@/api/crm/pool'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import ClaimDialog from './components/ClaimDialog.vue'
|
||||
import AssignDialog from './components/AssignDialog.vue'
|
||||
import CustomerDetail from '../CustomerDetailDialog.vue'
|
||||
@@ -628,7 +563,7 @@ const searchForm = reactive({
|
||||
const customerList = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const selectedRows = ref<any[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
// 多选状态
|
||||
const single = ref(true)
|
||||
@@ -644,113 +579,67 @@ const poolStatistics = reactive({
|
||||
})
|
||||
|
||||
// 列配置
|
||||
const COLUMN_STORAGE_KEY = 'crm_pool_visible_columns'
|
||||
|
||||
const tableColumns = ref([
|
||||
{ prop: 'shortName', label: '客户简称', fixed: false },
|
||||
{ prop: 'customerType', label: '客户类型', fixed: false },
|
||||
{ prop: 'mobile', label: '手机号', fixed: false },
|
||||
{ prop: 'telephone', label: '固定电话', fixed: false },
|
||||
{ prop: 'email', label: '邮箱', fixed: false },
|
||||
{ prop: 'website', label: '客户网站', fixed: false },
|
||||
{ prop: 'level', label: '客户级别', fixed: false },
|
||||
{ prop: 'source', label: '客户来源', fixed: false },
|
||||
{ prop: 'industry', label: '所属行业', fixed: false },
|
||||
{ prop: 'parkArea', label: '所在园区', fixed: false },
|
||||
{ prop: 'scale', label: '企业规模', fixed: false },
|
||||
{ prop: 'province', label: '省', fixed: false },
|
||||
{ prop: 'city', label: '市', fixed: false },
|
||||
{ prop: 'district', label: '区', fixed: false },
|
||||
{ prop: 'address', label: '详细地址', fixed: false },
|
||||
{ prop: 'status', label: '客户状态', fixed: false },
|
||||
{ prop: 'dealStatus', label: '成交状态', fixed: false },
|
||||
{ prop: 'ownerUserName', label: '原负责人', fixed: false },
|
||||
{ prop: 'followCount', label: '跟进次数', fixed: false },
|
||||
{ prop: 'lastTime', label: '最后跟进时间', fixed: false },
|
||||
{ prop: 'nextTime', label: '下次联系时间', fixed: false },
|
||||
{ prop: 'dealTime', label: '成交时间', fixed: false },
|
||||
{ prop: 'dealAmount', label: '成交金额', fixed: false },
|
||||
{ prop: 'contactCount', label: '联系人数量', fixed: false },
|
||||
{ prop: 'opportunityCount', label: '商机数量', fixed: false },
|
||||
{ prop: 'contractCount', label: '合同数量', fixed: false },
|
||||
{ prop: 'intoPoolTime', label: '进入公海时间', fixed: false },
|
||||
{ prop: 'claimTime', label: '领取时间', fixed: false },
|
||||
{ prop: 'protectTime', label: '保护到期时间', fixed: false },
|
||||
{ prop: 'createDeptName', label: '创建部门', fixed: false },
|
||||
{ prop: 'createByName', label: '创建人', fixed: false },
|
||||
{ prop: 'createTime', label: '创建时间', fixed: false },
|
||||
{ prop: 'updateByName', label: '更新人', fixed: false },
|
||||
{ prop: 'updateTime', label: '更新时间', fixed: false },
|
||||
{ prop: 'remark', label: '备注', fixed: false },
|
||||
])
|
||||
|
||||
const defaultVisibleColumns = [
|
||||
'customerType',
|
||||
'mobile',
|
||||
'level',
|
||||
'industry',
|
||||
'followCount',
|
||||
'lastTime',
|
||||
'intoPoolTime',
|
||||
'ownerUserName',
|
||||
const listColumnDefs = [
|
||||
{ prop: 'shortName', label: '客户简称' },
|
||||
{ prop: 'customerType', label: '客户类型' },
|
||||
{ prop: 'mobile', label: '手机号' },
|
||||
{ prop: 'telephone', label: '固定电话' },
|
||||
{ prop: 'email', label: '邮箱' },
|
||||
{ prop: 'website', label: '客户网站' },
|
||||
{ prop: 'level', label: '客户级别' },
|
||||
{ prop: 'source', label: '客户来源' },
|
||||
{ prop: 'industry', label: '所属行业' },
|
||||
{ prop: 'parkArea', label: '所在园区' },
|
||||
{ prop: 'scale', label: '企业规模' },
|
||||
{ prop: 'province', label: '省' },
|
||||
{ prop: 'city', label: '市' },
|
||||
{ prop: 'district', label: '区' },
|
||||
{ prop: 'address', label: '详细地址' },
|
||||
{ prop: 'status', label: '客户状态' },
|
||||
{ prop: 'dealStatus', label: '成交状态' },
|
||||
{ prop: 'ownerUserName', label: '原负责人' },
|
||||
{ prop: 'followCount', label: '跟进次数' },
|
||||
{ prop: 'lastTime', label: '最后跟进时间' },
|
||||
{ prop: 'nextTime', label: '下次联系时间' },
|
||||
{ prop: 'dealTime', label: '成交时间' },
|
||||
{ prop: 'dealAmount', label: '成交金额' },
|
||||
{ prop: 'contactCount', label: '联系人数量' },
|
||||
{ prop: 'opportunityCount', label: '商机数量' },
|
||||
{ prop: 'contractCount', label: '合同数量' },
|
||||
{ prop: 'intoPoolTime', label: '进入公海时间' },
|
||||
{ prop: 'claimTime', label: '领取时间' },
|
||||
{ prop: 'protectTime', label: '保护到期时间' },
|
||||
{ prop: 'createDeptName', label: '创建部门' },
|
||||
{ prop: 'createByName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'updateByName', label: '更新人' },
|
||||
{ prop: 'updateTime', label: '更新时间' },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
]
|
||||
|
||||
const visibleColumns = ref<string[]>([])
|
||||
const columnSettingDialog = reactive({ visible: false })
|
||||
const checkAll = ref(false)
|
||||
const isIndeterminate = ref(false)
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('crm_pool_visible_columns', listColumnDefs, {
|
||||
defaultVisibleProps: [
|
||||
'customerType',
|
||||
'mobile',
|
||||
'level',
|
||||
'industry',
|
||||
'followCount',
|
||||
'lastTime',
|
||||
'intoPoolTime',
|
||||
'ownerUserName',
|
||||
]
|
||||
})
|
||||
|
||||
const claimDialogRef = ref()
|
||||
const assignDialogRef = ref()
|
||||
const customerDetailRef = ref()
|
||||
|
||||
const initColumnSetting = () => {
|
||||
const saved = localStorage.getItem(COLUMN_STORAGE_KEY)
|
||||
if (saved) {
|
||||
try {
|
||||
visibleColumns.value = JSON.parse(saved)
|
||||
} catch {
|
||||
visibleColumns.value = [...defaultVisibleColumns]
|
||||
}
|
||||
} else {
|
||||
visibleColumns.value = [...defaultVisibleColumns]
|
||||
}
|
||||
updateCheckAllStatus()
|
||||
}
|
||||
|
||||
const isColumnVisible = (prop: string) => visibleColumns.value.includes(prop)
|
||||
|
||||
const handleColumnSetting = () => {
|
||||
columnSettingDialog.visible = true
|
||||
}
|
||||
|
||||
const handleCheckAllChange = (val: boolean) => {
|
||||
visibleColumns.value = val ? tableColumns.value.map((col) => col.prop) : []
|
||||
isIndeterminate.value = false
|
||||
}
|
||||
|
||||
const handleCheckedColumnsChange = () => {
|
||||
updateCheckAllStatus()
|
||||
}
|
||||
|
||||
const updateCheckAllStatus = () => {
|
||||
const checkedCount = visibleColumns.value.length
|
||||
const totalCount = tableColumns.value.length
|
||||
checkAll.value = checkedCount === totalCount
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < totalCount
|
||||
}
|
||||
|
||||
const handleResetColumns = () => {
|
||||
visibleColumns.value = [...defaultVisibleColumns]
|
||||
updateCheckAllStatus()
|
||||
}
|
||||
|
||||
const handleSaveColumnSetting = () => {
|
||||
localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(visibleColumns.value))
|
||||
ElMessage.success('列设置保存成功')
|
||||
columnSettingDialog.visible = false
|
||||
}
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -826,9 +715,7 @@ const handleAssign = (row?: any) => {
|
||||
}
|
||||
|
||||
const handleMoreCommand = (cmd: string, row: any) => {
|
||||
if (cmd === 'view') {
|
||||
handleView(row)
|
||||
} else if (cmd === 'assign') {
|
||||
if (cmd === 'assign') {
|
||||
handleAssign(row)
|
||||
}
|
||||
}
|
||||
@@ -843,7 +730,6 @@ watchDebounced(
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initColumnSetting()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
@@ -877,22 +763,4 @@ onMounted(() => {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.column-setting-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
.column-item {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="app-container opportunity-funnel">
|
||||
<!-- 筛选条件 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form :model="queryParams" ref="queryFormRef" label-width="90px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<div class="page-container opportunity-funnel-page">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">销售漏斗</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form ref="queryFormRef" class="filter-form" :model="queryParams" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="负责人" prop="ownerUserId">
|
||||
<el-select v-model="queryParams.ownerUserId" placeholder="请选择负责人" clearable style="width: 100%">
|
||||
<!-- TODO: 从用户列表获取 -->
|
||||
<el-select v-model="queryParams.ownerUserId" placeholder="请选择负责人" clearable>
|
||||
<el-option label="全部" :value="undefined" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="8" :lg="12" :md="16" :sm="24" :xs="24">
|
||||
<el-form-item label="时间范围" prop="timeRange">
|
||||
<el-date-picker
|
||||
v-model="queryParams.timeRange"
|
||||
@@ -24,198 +24,185 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24" style="text-align: right">
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 概览统计 -->
|
||||
<el-row :gutter="16" class="summary-row">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="summary-card">
|
||||
<div class="card-icon" style="background: #e6f7ff">
|
||||
<el-icon :size="32" color="#1890ff"><Opportunity /></el-icon>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-value">{{ summary.totalOpportunities }}</div>
|
||||
<div class="card-label">商机总数</div>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="resetQuery">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="summary-card">
|
||||
<div class="card-icon" style="background: #f0f9ff">
|
||||
<el-icon :size="32" color="#67c23a"><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-value success">{{ summary.wonOpportunities }}</div>
|
||||
<div class="card-label">赢单数量</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="summary-card">
|
||||
<div class="card-icon" style="background: #fff7e6">
|
||||
<el-icon :size="32" color="#e6a23c"><TrendCharts /></el-icon>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-value warning">{{ summary.overallConversionRate }}%</div>
|
||||
<div class="card-label">总转化率</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-card shadow="hover" class="summary-card">
|
||||
<div class="card-icon" style="background: #f0f9ff">
|
||||
<el-icon :size="32" color="#409eff"><Money /></el-icon>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-value primary">¥{{ formatAmount(summary.totalAmount) }}</div>
|
||||
<div class="card-label">商机总金额</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 漏斗图 -->
|
||||
<el-card shadow="never" class="funnel-card">
|
||||
<template #header>
|
||||
<span class="card-title">销售漏斗分析</span>
|
||||
</template>
|
||||
<div v-loading="loading" class="funnel-container">
|
||||
<div ref="funnelChartRef" class="funnel-chart"></div>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 阶段明细表格 -->
|
||||
<el-card shadow="never" class="detail-card">
|
||||
<template #header>
|
||||
<span class="card-title">阶段转化明细</span>
|
||||
</template>
|
||||
<el-table :data="funnelData" border>
|
||||
<el-table-column label="商机阶段" prop="stageName" align="center" />
|
||||
<el-table-column label="商机数量" prop="count" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag type="primary">{{ scope.row.count }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商机金额" prop="totalAmount" align="right" header-align="right">
|
||||
<template #default="scope">
|
||||
<span class="amount-text">¥{{ formatAmount(scope.row.totalAmount) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="平均赢单率" prop="avgWinRate" align="center">
|
||||
<template #default="scope"> {{ scope.row.avgWinRate }}% </template>
|
||||
</el-table-column>
|
||||
<el-table-column label="转化率" prop="conversionRate" align="center">
|
||||
<template #default="scope">
|
||||
<el-progress :percentage="parseFloat(scope.row.conversionRate)" :color="getProgressColor(scope.row.conversionRate)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<el-icon><Opportunity /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">商机总数</div>
|
||||
<div class="stat-value">{{ summary.totalOpportunities }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success">
|
||||
<el-icon><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">赢单数量</div>
|
||||
<div class="stat-value">{{ summary.wonOpportunities }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">总转化率</div>
|
||||
<div class="stat-value">{{ summary.overallConversionRate }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon primary">
|
||||
<el-icon><Money /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-title">商机总金额</div>
|
||||
<div class="stat-value money">¥{{ formatAmount(summary.totalAmount) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-panel chart-panel-full">
|
||||
<div class="chart-panel-head">
|
||||
<span class="chart-title">销售漏斗分析</span>
|
||||
</div>
|
||||
<div v-loading="loading" class="chart-container chart-container-tall">
|
||||
<div ref="funnelChartRef" class="funnel-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-panel chart-panel-full">
|
||||
<div class="chart-panel-head">
|
||||
<span class="chart-title">阶段转化明细</span>
|
||||
</div>
|
||||
<div class="list-body list-wrap">
|
||||
<el-table class="list-table" :data="funnelData" stripe>
|
||||
<el-table-column label="商机阶段" prop="stageName" min-width="120" align="center" />
|
||||
<el-table-column label="商机数量" prop="count" min-width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag type="primary" size="small">{{ scope.row.count }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商机金额" prop="totalAmount" min-width="140" align="right" class-name="col-money">
|
||||
<template #default="scope">¥{{ formatAmount(scope.row.totalAmount) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="平均赢单率" prop="avgWinRate" min-width="110" align="center">
|
||||
<template #default="scope">{{ scope.row.avgWinRate }}%</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="转化率" prop="conversionRate" min-width="180" align="center">
|
||||
<template #default="scope">
|
||||
<el-progress
|
||||
:percentage="parseFloat(scope.row.conversionRate)"
|
||||
:color="getProgressColor(scope.row.conversionRate)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="OpportunityFunnel">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue';
|
||||
import { getSalesFunnel } from '@/api/crm/opportunity';
|
||||
import * as echarts from 'echarts';
|
||||
import type { ECharts } from 'echarts';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Opportunity, CircleCheck, TrendCharts, Money } from '@element-plus/icons-vue';
|
||||
import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { getSalesFunnel } from '@/api/crm/opportunity'
|
||||
import * as echarts from 'echarts'
|
||||
import type { ECharts } from 'echarts'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Opportunity, CircleCheck, TrendCharts, Money, Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
|
||||
const loading = ref(false);
|
||||
const queryFormRef = ref();
|
||||
const funnelChartRef = ref();
|
||||
let funnelChart: ECharts | null = null;
|
||||
const MINT_DARK = '#059669'
|
||||
const MINT_PALETTE = ['#10b981', '#34d399', '#059669', '#047857', '#6ee7b7', '#0d9488', '#14b8a6']
|
||||
|
||||
const loading = ref(false)
|
||||
const queryFormRef = ref()
|
||||
const funnelChartRef = ref<HTMLElement>()
|
||||
let funnelChart: ECharts | null = null
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref<{
|
||||
ownerUserId?: number;
|
||||
timeRange?: string[];
|
||||
ownerUserId?: number
|
||||
timeRange?: string[]
|
||||
}>({
|
||||
timeRange: []
|
||||
});
|
||||
})
|
||||
|
||||
// 汇总数据
|
||||
const summary = reactive({
|
||||
totalOpportunities: 0,
|
||||
wonOpportunities: 0,
|
||||
overallConversionRate: '0.00',
|
||||
totalAmount: 0
|
||||
});
|
||||
})
|
||||
|
||||
// 漏斗数据
|
||||
const funnelData = ref<any[]>([]);
|
||||
const funnelData = ref<any[]>([])
|
||||
|
||||
/** 加载漏斗数据 */
|
||||
const loadFunnelData = async () => {
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
ownerUserId: queryParams.value.ownerUserId
|
||||
};
|
||||
|
||||
if (queryParams.value.timeRange && queryParams.value.timeRange.length === 2) {
|
||||
params.beginTime = queryParams.value.timeRange[0];
|
||||
params.endTime = queryParams.value.timeRange[1];
|
||||
}
|
||||
|
||||
const res = await getSalesFunnel(params);
|
||||
if (queryParams.value.timeRange && queryParams.value.timeRange.length === 2) {
|
||||
params.beginTime = queryParams.value.timeRange[0]
|
||||
params.endTime = queryParams.value.timeRange[1]
|
||||
}
|
||||
|
||||
// 更新汇总数据
|
||||
Object.assign(summary, res.data.summary || {});
|
||||
const res = await getSalesFunnel(params)
|
||||
Object.assign(summary, res.data.summary || {})
|
||||
funnelData.value = res.data.funnelData || []
|
||||
|
||||
// 更新漏斗数据
|
||||
funnelData.value = res.data.funnelData || [];
|
||||
|
||||
// 渲染漏斗图
|
||||
await nextTick();
|
||||
renderFunnelChart();
|
||||
await nextTick()
|
||||
renderFunnelChart()
|
||||
} catch (error) {
|
||||
console.error('加载漏斗数据失败:', error);
|
||||
ElMessage.error('加载漏斗数据失败');
|
||||
console.error('加载漏斗数据失败:', error)
|
||||
ElMessage.error('加载漏斗数据失败')
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 渲染漏斗图 */
|
||||
const renderFunnelChart = () => {
|
||||
if (!funnelChartRef.value) return;
|
||||
if (!funnelChartRef.value) return
|
||||
|
||||
// 初始化图表
|
||||
if (!funnelChart) {
|
||||
funnelChart = echarts.init(funnelChartRef.value);
|
||||
funnelChart = echarts.init(funnelChartRef.value)
|
||||
}
|
||||
|
||||
// 准备数据
|
||||
const chartData = funnelData.value.map((item) => ({
|
||||
name: `${item.stageName} (${item.count})`,
|
||||
value: item.count
|
||||
}));
|
||||
}))
|
||||
|
||||
// 配置项
|
||||
const option = {
|
||||
funnelChart.setOption({
|
||||
title: {
|
||||
text: '商机阶段转化漏斗',
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 600
|
||||
fontWeight: 600,
|
||||
color: '#1e293b'
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: (params: any) => {
|
||||
const data = funnelData.value[params.dataIndex];
|
||||
const data = funnelData.value[params.dataIndex]
|
||||
return `
|
||||
<div style="padding: 8px;">
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">${data.stageName}</div>
|
||||
@@ -224,12 +211,10 @@ const renderFunnelChart = () => {
|
||||
<div>平均赢单率: ${data.avgWinRate}%</div>
|
||||
<div>转化率: ${data.conversionRate}%</div>
|
||||
</div>
|
||||
`;
|
||||
`
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
},
|
||||
legend: { show: false },
|
||||
series: [
|
||||
{
|
||||
name: '商机漏斗',
|
||||
@@ -253,152 +238,170 @@ const renderFunnelChart = () => {
|
||||
},
|
||||
labelLine: {
|
||||
length: 10,
|
||||
lineStyle: {
|
||||
width: 1,
|
||||
type: 'solid'
|
||||
}
|
||||
lineStyle: { width: 1, type: 'solid' }
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
fontSize: 16
|
||||
}
|
||||
label: { fontSize: 16 }
|
||||
},
|
||||
data: chartData,
|
||||
// 颜色渐变
|
||||
color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452']
|
||||
color: MINT_PALETTE
|
||||
}
|
||||
]
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
funnelChart.setOption(option);
|
||||
|
||||
// 响应式
|
||||
window.addEventListener('resize', () => {
|
||||
funnelChart?.resize();
|
||||
});
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
loadFunnelData();
|
||||
};
|
||||
loadFunnelData()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
loadFunnelData();
|
||||
};
|
||||
queryFormRef.value?.resetFields()
|
||||
loadFunnelData()
|
||||
}
|
||||
|
||||
/** 格式化金额 */
|
||||
const formatAmount = (amount: number) => {
|
||||
if (!amount) return '0.00';
|
||||
if (!amount) return '0.00'
|
||||
return amount.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取进度条颜色 */
|
||||
const getProgressColor = (rate: number) => {
|
||||
const numRate = parseFloat(rate);
|
||||
if (numRate >= 70) return '#67c23a';
|
||||
if (numRate >= 40) return '#e6a23c';
|
||||
return '#f56c6c';
|
||||
};
|
||||
const getProgressColor = (rate: number | string) => {
|
||||
const numRate = parseFloat(String(rate))
|
||||
if (numRate >= 70) return MINT_DARK
|
||||
if (numRate >= 40) return '#d97706'
|
||||
return '#dc2626'
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
funnelChart?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFunnelData();
|
||||
});
|
||||
loadFunnelData()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
funnelChart?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.opportunity-funnel {
|
||||
.filter-card {
|
||||
margin-bottom: 16px;
|
||||
.opportunity-funnel-page {
|
||||
.stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
padding: 0 16px 16px;
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
margin-bottom: 16px;
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: var(--mint-surface, #fff);
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
border-radius: var(--mint-radius, 10px);
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
:deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
.stat-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin-right: 14px;
|
||||
|
||||
.card-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
&.primary {
|
||||
background: var(--primary-bg, #ecfdf5);
|
||||
color: var(--primary, #10b981);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
&.success {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: var(--primary-dark, #059669);
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
&.warning {
|
||||
background: rgba(217, 119, 6, 0.12);
|
||||
color: #d97706;
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #67c23a;
|
||||
}
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&.warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
.stat-title {
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: var(--mint-text, #1e293b);
|
||||
line-height: 1.2;
|
||||
|
||||
.card-label {
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
}
|
||||
&.money {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.funnel-card {
|
||||
margin-bottom: 16px;
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.funnel-container {
|
||||
min-height: 500px;
|
||||
|
||||
.funnel-chart {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
.chart-panel {
|
||||
background: var(--mint-surface, #fff);
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
border-radius: var(--mint-radius, 10px);
|
||||
overflow: hidden;
|
||||
margin: 0 16px 16px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.chart-panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--mint-border, #d9e8e2);
|
||||
}
|
||||
|
||||
.amount-text {
|
||||
color: #67c23a;
|
||||
font-weight: 600;
|
||||
}
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #1e293b);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
min-height: 360px;
|
||||
padding: 8px 12px 16px;
|
||||
}
|
||||
|
||||
.chart-container-tall {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.funnel-chart {
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,12 +6,14 @@
|
||||
<h2 class="title-bar-heading">商机管理</h2>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs">
|
||||
<el-tabs v-model="activeView" @tab-change="handleViewChange">
|
||||
<el-tab-pane label="我的商机" name="mine" />
|
||||
<el-tab-pane label="全部商机" name="all" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-tabs
|
||||
v-model="activeView"
|
||||
class="panel-pill-tabs panel-pill-tabs--lg"
|
||||
@tab-change="handleViewChange"
|
||||
>
|
||||
<el-tab-pane label="我的商机" name="mine" />
|
||||
<el-tab-pane label="全部商机" name="all" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form ref="queryFormRef" :model="queryParams" class="filter-form" @submit.prevent>
|
||||
@@ -119,6 +121,28 @@
|
||||
>
|
||||
<el-icon><Plus /></el-icon>新增商机
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasOpportunityPerm('edit')"
|
||||
class="btn-ghost"
|
||||
:disabled="
|
||||
selectedRows.length !== 1 ||
|
||||
selectedRows[0]?.status === 'won' ||
|
||||
selectedRows[0]?.status === 'lost'
|
||||
"
|
||||
@click="handleUpdate()"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasOpportunityPerm('remove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length !== 1 || !canDeleteByCreator(selectedRows[0])"
|
||||
:title="canDeleteByCreator(selectedRows[0]) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasOpportunityPerm('assign')"
|
||||
class="btn-ghost"
|
||||
@@ -127,6 +151,16 @@
|
||||
>
|
||||
<el-icon><User /></el-icon>批量分配
|
||||
</el-button>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -314,23 +348,6 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column v-if="isColumnVisible('remark')" label="备注" align="center" header-align="center" prop="remark" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" min-width="200" align="center" header-align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button v-if="hasOpportunityPerm('query')" class="btn-op-item" @click="handleDetail(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasOpportunityPerm('edit')"
|
||||
class="btn-op-item primary"
|
||||
:disabled="row.status === 'won' || row.status === 'lost'"
|
||||
@click="handleUpdate(row)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -402,46 +419,12 @@
|
||||
|
||||
<!-- 商机详情弹窗 -->
|
||||
<OpportunityDetailDrawer v-model="detailVisible" :opportunity-id="currentId" @success="getList" />
|
||||
|
||||
<!-- 列设置对话框 -->
|
||||
<el-dialog
|
||||
v-model="columnSettingDialog.visible"
|
||||
title="列设置"
|
||||
width="600px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="column-setting-content">
|
||||
<el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" @change="handleCheckAllChange">全选</el-checkbox>
|
||||
<el-divider />
|
||||
<el-checkbox-group v-model="visibleColumns" @change="handleCheckedColumnsChange">
|
||||
<div v-for="col in tableColumns" :key="col.prop" class="column-item">
|
||||
<el-checkbox :label="col.prop" :disabled="col.fixed">{{ col.label }}</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button class="btn-ghost" @click="columnSettingDialog.visible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handleResetColumns">
|
||||
<el-icon><RefreshLeft /></el-icon>恢复默认
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSaveColumnSetting">
|
||||
<el-icon><Check /></el-icon>保存
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="CrmOpportunity">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { ElForm } from 'element-plus'
|
||||
@@ -454,12 +437,12 @@ import {
|
||||
User,
|
||||
ArrowUpBold,
|
||||
ArrowDownBold,
|
||||
View,
|
||||
Edit
|
||||
Edit,
|
||||
Delete
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { useOpportunityPerm } from '@/hooks/business/useOpportunityPerm'
|
||||
import { filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { filterDeletableByCreator, canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { fetchUserOptions } from '@/api/admin'
|
||||
import { listOpportunity, listMyOpportunity, delOpportunity, assignOpportunity, exportOpportunity } from '@/api/crm/opportunity'
|
||||
import type { OpportunityQuery, OpportunityVO } from '@/api/crm/opportunity/types'
|
||||
@@ -471,6 +454,8 @@ import FollowDialog from './components/FollowDialog.vue'
|
||||
import BatchUpdateStageDialog from './components/BatchUpdateStageDialog.vue'
|
||||
import BatchUpdateDateDialog from './components/BatchUpdateDateDialog.vue'
|
||||
import OpportunityDetailDrawer from './components/OpportunityDetailDrawer.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const router = useRouter()
|
||||
@@ -481,127 +466,56 @@ const selectedRows = ref<OpportunityVO[]>([])
|
||||
const exportLoading = ref(false)
|
||||
const assignLoading = ref(false)
|
||||
|
||||
// 列配置相关
|
||||
const COLUMN_STORAGE_KEY = 'crm_opportunity_visible_columns';
|
||||
// 列配置
|
||||
const listColumnDefs = [
|
||||
{ prop: 'customerName', label: '客户名称' },
|
||||
{ prop: 'contactName', label: '联系人' },
|
||||
{ prop: 'amount', label: '商机金额' },
|
||||
{ prop: 'stage', label: '商机阶段' },
|
||||
{ prop: 'stagePercent', label: '阶段进度' },
|
||||
{ prop: 'status', label: '商机状态' },
|
||||
{ prop: 'winRate', label: '赢单率' },
|
||||
{ prop: 'expectDealDate', label: '预计成交日期' },
|
||||
{ prop: 'actualDealDate', label: '实际成交日期' },
|
||||
{ prop: 'source', label: '商机来源' },
|
||||
{ prop: 'type', label: '商机类型' },
|
||||
{ prop: 'description', label: '商机描述' },
|
||||
{ prop: 'competitors', label: '竞争对手' },
|
||||
{ prop: 'keyFactors', label: '关键决策因素' },
|
||||
{ prop: 'lostReason', label: '输单原因' },
|
||||
{ prop: 'ownerUserName', label: '负责人' },
|
||||
{ prop: 'productCount', label: '产品数量' },
|
||||
{ prop: 'followCount', label: '跟进次数' },
|
||||
{ prop: 'lastTime', label: '最后跟进时间' },
|
||||
{ prop: 'nextTime', label: '下次联系时间' },
|
||||
{ prop: 'stageChangeTime', label: '阶段变更时间' },
|
||||
{ prop: 'createDeptName', label: '创建部门' },
|
||||
{ prop: 'createByName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'updateByName', label: '更新人' },
|
||||
{ prop: 'updateTime', label: '更新时间' },
|
||||
{ prop: 'remark', label: '备注' }
|
||||
]
|
||||
|
||||
// 定义所有列配置(顺序代表显示顺序)
|
||||
const tableColumns = ref([
|
||||
{ prop: 'customerName', label: '客户名称', fixed: false },
|
||||
{ prop: 'contactName', label: '联系人', fixed: false },
|
||||
{ prop: 'amount', label: '商机金额', fixed: false },
|
||||
{ prop: 'stage', label: '商机阶段', fixed: false },
|
||||
{ prop: 'stagePercent', label: '阶段进度', fixed: false },
|
||||
{ prop: 'status', label: '商机状态', fixed: false },
|
||||
{ prop: 'winRate', label: '赢单率', fixed: false },
|
||||
{ prop: 'expectDealDate', label: '预计成交日期', fixed: false },
|
||||
{ prop: 'actualDealDate', label: '实际成交日期', fixed: false },
|
||||
{ prop: 'source', label: '商机来源', fixed: false },
|
||||
{ prop: 'type', label: '商机类型', fixed: false },
|
||||
{ prop: 'description', label: '商机描述', fixed: false },
|
||||
{ prop: 'competitors', label: '竞争对手', fixed: false },
|
||||
{ prop: 'keyFactors', label: '关键决策因素', fixed: false },
|
||||
{ prop: 'lostReason', label: '输单原因', fixed: false },
|
||||
{ prop: 'ownerUserName', label: '负责人', fixed: false },
|
||||
{ prop: 'productCount', label: '产品数量', fixed: false },
|
||||
{ prop: 'followCount', label: '跟进次数', fixed: false },
|
||||
{ prop: 'lastTime', label: '最后跟进时间', fixed: false },
|
||||
{ prop: 'nextTime', label: '下次联系时间', fixed: false },
|
||||
{ prop: 'stageChangeTime', label: '阶段变更时间', fixed: false },
|
||||
{ prop: 'createDeptName', label: '创建部门', fixed: false },
|
||||
{ prop: 'createByName', label: '创建人', fixed: false },
|
||||
{ prop: 'createTime', label: '创建时间', fixed: false },
|
||||
{ prop: 'updateByName', label: '更新人', fixed: false },
|
||||
{ prop: 'updateTime', label: '更新时间', fixed: false },
|
||||
{ prop: 'remark', label: '备注', fixed: false }
|
||||
]);
|
||||
|
||||
// 默认显示的列(现有字段)
|
||||
const defaultVisibleColumns = [
|
||||
'customerName',
|
||||
'stage',
|
||||
'stagePercent',
|
||||
'winRate',
|
||||
'expectDealDate',
|
||||
'status',
|
||||
'ownerUserName',
|
||||
'followCount',
|
||||
'createTime'
|
||||
];
|
||||
|
||||
// 当前显示的列
|
||||
const visibleColumns = ref<string[]>([]);
|
||||
|
||||
// 列设置对话框
|
||||
const columnSettingDialog = reactive({
|
||||
visible: false
|
||||
});
|
||||
|
||||
// 全选状态
|
||||
const checkAll = ref(false);
|
||||
const isIndeterminate = ref(false);
|
||||
|
||||
/** 初始化列配置 */
|
||||
const initColumnSetting = () => {
|
||||
const savedColumns = localStorage.getItem(COLUMN_STORAGE_KEY);
|
||||
if (savedColumns) {
|
||||
try {
|
||||
visibleColumns.value = JSON.parse(savedColumns);
|
||||
} catch (error) {
|
||||
console.error('解析列配置失败:', error);
|
||||
visibleColumns.value = [...defaultVisibleColumns];
|
||||
}
|
||||
} else {
|
||||
visibleColumns.value = [...defaultVisibleColumns];
|
||||
}
|
||||
updateCheckAllStatus();
|
||||
};
|
||||
|
||||
/** 判断列是否显示 */
|
||||
const isColumnVisible = (prop: string) => {
|
||||
return visibleColumns.value.includes(prop);
|
||||
};
|
||||
|
||||
/** 打开列设置对话框 */
|
||||
const handleColumnSetting = () => {
|
||||
columnSettingDialog.visible = true;
|
||||
};
|
||||
|
||||
/** 全选/取消全选 */
|
||||
const handleCheckAllChange = (val: boolean) => {
|
||||
visibleColumns.value = val ? tableColumns.value.map((col) => col.prop) : [];
|
||||
isIndeterminate.value = false;
|
||||
};
|
||||
|
||||
/** 列选择变化 */
|
||||
const handleCheckedColumnsChange = () => {
|
||||
updateCheckAllStatus();
|
||||
};
|
||||
|
||||
/** 更新全选状态 */
|
||||
const updateCheckAllStatus = () => {
|
||||
const checkedCount = visibleColumns.value.length;
|
||||
const totalCount = tableColumns.value.length;
|
||||
checkAll.value = checkedCount === totalCount;
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < totalCount;
|
||||
};
|
||||
|
||||
/** 恢复默认列设置 */
|
||||
const handleResetColumns = () => {
|
||||
visibleColumns.value = [...defaultVisibleColumns];
|
||||
updateCheckAllStatus();
|
||||
};
|
||||
|
||||
/** 保存列设置 */
|
||||
const handleSaveColumnSetting = () => {
|
||||
try {
|
||||
localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(visibleColumns.value));
|
||||
ElMessage.success('列设置保存成功')
|
||||
columnSettingDialog.visible = false;
|
||||
} catch (error) {
|
||||
console.error('保存列配置失败:', error);
|
||||
ElMessage.error('保存列设置失败')
|
||||
}
|
||||
};
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('crm_opportunity_visible_columns', listColumnDefs, {
|
||||
defaultVisibleProps: [
|
||||
'customerName',
|
||||
'stage',
|
||||
'stagePercent',
|
||||
'winRate',
|
||||
'expectDealDate',
|
||||
'status',
|
||||
'ownerUserName',
|
||||
'followCount',
|
||||
'createTime'
|
||||
]
|
||||
})
|
||||
|
||||
// 当前视图
|
||||
const activeView = ref('mine');
|
||||
@@ -612,7 +526,7 @@ const viewMode = ref<'list' | 'kanban'>('list');
|
||||
// 查询参数
|
||||
const queryParams = ref<OpportunityQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
opportunityName: undefined,
|
||||
customerName: undefined,
|
||||
stage: undefined,
|
||||
@@ -1010,7 +924,6 @@ watchDebounced(
|
||||
// 初始化加载数据
|
||||
onMounted(async () => {
|
||||
await loadPerms()
|
||||
initColumnSetting();
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -1024,22 +937,4 @@ onMounted(async () => {
|
||||
.quick-search-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.column-setting-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
.column-item {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,20 +1,20 @@
|
||||
<template>
|
||||
<div class="app-container opportunity-kanban">
|
||||
<!-- 查询筛选栏 -->
|
||||
<el-card shadow="hover" class="mb-[10px]">
|
||||
<el-form :model="queryParams" ref="queryFormRef" label-width="90px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<div class="page-container opportunity-kanban-page">
|
||||
<section class="panel kanban-panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">商机看板</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form ref="queryFormRef" class="filter-form" :model="queryParams" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="负责人" prop="ownerUserId">
|
||||
<el-select v-model="queryParams.ownerUserId" placeholder="请选择负责人" clearable style="width: 100%">
|
||||
<!-- TODO: 从用户列表获取 -->
|
||||
<el-select v-model="queryParams.ownerUserId" placeholder="请选择负责人" clearable>
|
||||
<el-option label="全部" :value="undefined" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xl="4" :lg="6" :md="8" :sm="12" :xs="24">
|
||||
<el-form-item label="商机来源" prop="source">
|
||||
<el-select v-model="queryParams.source" placeholder="请选择商机来源" clearable style="width: 100%">
|
||||
<el-select v-model="queryParams.source" placeholder="请选择商机来源" clearable>
|
||||
<el-option label="全部" :value="undefined" />
|
||||
<el-option label="电话咨询" value="phone" />
|
||||
<el-option label="网络推广" value="online" />
|
||||
@@ -24,78 +24,82 @@
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24" style="text-align: right">
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 看板区域 -->
|
||||
<div class="kanban-container" v-loading="loading">
|
||||
<div v-for="stage in stageColumns" :key="stage.key" class="kanban-column">
|
||||
<!-- 列头 -->
|
||||
<div class="column-header">
|
||||
<div class="header-title">
|
||||
<span class="stage-name">{{ stage.name }}</span>
|
||||
<el-tag size="small" type="info">{{ stage.count }}</el-tag>
|
||||
</div>
|
||||
<div class="header-amount">¥{{ formatAmount(stage.totalAmount) }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片列表 -->
|
||||
<div class="column-content">
|
||||
<div v-for="opp in stage.opportunities" :key="opp.id" class="opportunity-card" @click="handleViewDetail(opp.id)">
|
||||
<div class="card-header">
|
||||
<span class="card-title">{{ opp.opportunityName }}</span>
|
||||
<el-dropdown trigger="click" @command="(cmd) => handleCardAction(cmd, opp)">
|
||||
<el-icon class="card-more"><MoreFilled /></el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">修改</el-dropdown-item>
|
||||
<el-dropdown-item command="stage">推进阶段</el-dropdown-item>
|
||||
<el-dropdown-item command="follow">添加跟进</el-dropdown-item>
|
||||
<el-dropdown-item command="won" divided>标记赢单</el-dropdown-item>
|
||||
<el-dropdown-item command="lost">标记输单</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="card-info">
|
||||
<div class="info-row">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>{{ opp.customerName }}</span>
|
||||
</div>
|
||||
<div class="info-row amount">
|
||||
<el-icon><Money /></el-icon>
|
||||
<span>¥{{ formatAmount(opp.amount) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
<span>{{ formatDate(opp.expectDealDate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<el-progress :percentage="opp.stagePercent" :stroke-width="4" :show-text="false" />
|
||||
<span class="win-rate">赢单率: {{ opp.winRate }}%</span>
|
||||
<div class="filter-actions">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="resetQuery">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>查询
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="stage.opportunities.length === 0" class="column-empty">
|
||||
<el-empty description="暂无商机" :image-size="80" />
|
||||
<div class="kanban-container" v-loading="loading">
|
||||
<div v-for="stage in stageColumns" :key="stage.key" class="kanban-column">
|
||||
<div class="column-header">
|
||||
<div class="header-title">
|
||||
<span class="stage-name">{{ stage.name }}</span>
|
||||
<el-tag size="small" type="info">{{ stage.count }}</el-tag>
|
||||
</div>
|
||||
<div class="header-amount">¥{{ formatAmount(stage.totalAmount) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="column-content">
|
||||
<div
|
||||
v-for="opp in stage.opportunities"
|
||||
:key="opp.id"
|
||||
class="opportunity-card"
|
||||
@click="handleViewDetail(opp.id)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<span class="card-title">{{ opp.opportunityName }}</span>
|
||||
<el-dropdown trigger="click" @command="(cmd) => handleCardAction(cmd, opp)">
|
||||
<el-icon class="card-more" @click.stop><MoreFilled /></el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit">修改</el-dropdown-item>
|
||||
<el-dropdown-item command="stage">推进阶段</el-dropdown-item>
|
||||
<el-dropdown-item command="follow">添加跟进</el-dropdown-item>
|
||||
<el-dropdown-item command="won" divided>标记赢单</el-dropdown-item>
|
||||
<el-dropdown-item command="lost">标记输单</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="card-info">
|
||||
<div class="info-row">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>{{ opp.customerName }}</span>
|
||||
</div>
|
||||
<div class="info-row amount">
|
||||
<el-icon><Money /></el-icon>
|
||||
<span>¥{{ formatAmount(opp.amount) }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
<span>{{ formatDate(opp.expectDealDate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<el-progress :percentage="opp.stagePercent" :stroke-width="4" :show-text="false" />
|
||||
<span class="win-rate">赢单率: {{ opp.winRate }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="stage.opportunities.length === 0" class="column-empty">
|
||||
<el-empty description="暂无商机" :image-size="80" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 对话框组件 -->
|
||||
<ChangeStageDialog ref="changeStageDialogRef" @success="loadKanbanData" />
|
||||
<MarkWonDialog ref="markWonDialogRef" @success="loadKanbanData" />
|
||||
<MarkLostDialog ref="markLostDialogRef" @success="loadKanbanData" />
|
||||
@@ -104,7 +108,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="OpportunityKanban">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { getKanbanData } from '@/api/crm/opportunity'
|
||||
@@ -114,140 +118,123 @@ import MarkWonDialog from './components/MarkWonDialog.vue'
|
||||
import MarkLostDialog from './components/MarkLostDialog.vue'
|
||||
import FollowDialog from './components/FollowDialog.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { MoreFilled, User, Money, Calendar } from '@element-plus/icons-vue'
|
||||
import { MoreFilled, User, Money, Calendar, Search, RefreshLeft } from '@element-plus/icons-vue'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const queryFormRef = ref()
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref<OpportunityQuery>({});
|
||||
const queryParams = ref<OpportunityQuery>({})
|
||||
|
||||
// 阶段颜色配置(补充字典中没有的颜色信息)
|
||||
const stageColors: Record<string, string> = {
|
||||
initial: '#909399',
|
||||
requirement: '#409EFF',
|
||||
solution: '#67C23A',
|
||||
quote: '#E6A23C',
|
||||
negotiate: '#F56C6C',
|
||||
contract: '#9C27B0'
|
||||
};
|
||||
|
||||
// 看板列数据
|
||||
interface StageColumn {
|
||||
key: string;
|
||||
name: string;
|
||||
color: string;
|
||||
count: number;
|
||||
totalAmount: number;
|
||||
opportunities: OpportunityVO[];
|
||||
initial: '#64748b',
|
||||
requirement: '#10b981',
|
||||
solution: '#059669',
|
||||
quote: '#d97706',
|
||||
negotiate: '#047857',
|
||||
contract: '#0d9488'
|
||||
}
|
||||
|
||||
const stageColumns = ref<StageColumn[]>([]);
|
||||
interface StageColumn {
|
||||
key: string
|
||||
name: string
|
||||
color: string
|
||||
count: number
|
||||
totalAmount: number
|
||||
opportunities: OpportunityVO[]
|
||||
}
|
||||
|
||||
// 对话框引用
|
||||
const changeStageDialogRef = ref();
|
||||
const markWonDialogRef = ref();
|
||||
const markLostDialogRef = ref();
|
||||
const followDialogRef = ref();
|
||||
const stageColumns = ref<StageColumn[]>([])
|
||||
|
||||
const changeStageDialogRef = ref()
|
||||
const markWonDialogRef = ref()
|
||||
const markLostDialogRef = ref()
|
||||
const followDialogRef = ref()
|
||||
|
||||
/** 加载看板数据 */
|
||||
const loadKanbanData = async () => {
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getKanbanData(queryParams.value);
|
||||
|
||||
// 转换数据格式,使用字典数据
|
||||
stageColumns.value = dictStore.getOptions('crm_opportunity_stage')
|
||||
.filter((stage: any) => stage.value !== 'won') // 排除赢单阶段(已完成的商机)
|
||||
const res = await getKanbanData(queryParams.value)
|
||||
stageColumns.value = dictStore
|
||||
.getOptions('crm_opportunity_stage')
|
||||
.filter((stage: any) => stage.value !== 'won')
|
||||
.map((stage: any) => {
|
||||
const stageData = res.data[stage.value] || {};
|
||||
const stageData = res.data[stage.value] || {}
|
||||
return {
|
||||
key: stage.value,
|
||||
name: stage.label,
|
||||
color: stageColors[stage.value] || '#909399',
|
||||
color: stageColors[stage.value] || '#64748b',
|
||||
count: stageData.count || 0,
|
||||
totalAmount: stageData.totalAmount || 0,
|
||||
opportunities: stageData.opportunities || []
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('加载看板数据失败:', error);
|
||||
ElMessage.error('加载看板数据失败');
|
||||
console.error('加载看板数据失败:', error)
|
||||
ElMessage.error('加载看板数据失败')
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
loadKanbanData();
|
||||
};
|
||||
loadKanbanData()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
loadKanbanData();
|
||||
};
|
||||
queryFormRef.value?.resetFields()
|
||||
loadKanbanData()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
const handleViewDetail = (id: number) => {
|
||||
router.push({ path: '/crm/opportunity/detail', query: { id } });
|
||||
};
|
||||
router.push({ path: '/crm/opportunity/detail', query: { id } })
|
||||
}
|
||||
|
||||
/** 卡片操作 */
|
||||
const handleCardAction = (command: string, opp: OpportunityVO) => {
|
||||
switch (command) {
|
||||
case 'edit':
|
||||
router.push({ path: '/crm/opportunity', query: { id: opp.id, action: 'edit' } });
|
||||
break;
|
||||
router.push({ path: '/crm/opportunity', query: { id: opp.id, action: 'edit' } })
|
||||
break
|
||||
case 'stage':
|
||||
changeStageDialogRef.value?.open(opp.id);
|
||||
break;
|
||||
changeStageDialogRef.value?.open(opp.id)
|
||||
break
|
||||
case 'follow':
|
||||
followDialogRef.value?.open(opp.id);
|
||||
break;
|
||||
followDialogRef.value?.open(opp.id)
|
||||
break
|
||||
case 'won':
|
||||
markWonDialogRef.value?.open(opp.id);
|
||||
break;
|
||||
markWonDialogRef.value?.open(opp.id)
|
||||
break
|
||||
case 'lost':
|
||||
markLostDialogRef.value?.open(opp.id);
|
||||
break;
|
||||
markLostDialogRef.value?.open(opp.id)
|
||||
break
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** 格式化金额 */
|
||||
const formatAmount = (amount: number) => {
|
||||
if (!amount) return '0.00';
|
||||
if (!amount) return '0.00'
|
||||
return amount.toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/** 格式化日期 */
|
||||
const formatDate = (date: string) => {
|
||||
if (!date) return '-';
|
||||
return date.split(' ')[0];
|
||||
};
|
||||
if (!date) return '-'
|
||||
return date.split(' ')[0]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadKanbanData();
|
||||
});
|
||||
loadKanbanData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.opportunity-kanban {
|
||||
height: calc(100vh - 120px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.kanban-filter {
|
||||
background: #fff;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 4px;
|
||||
.opportunity-kanban-page {
|
||||
.kanban-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.kanban-container {
|
||||
@@ -255,141 +242,145 @@ onMounted(() => {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 16px;
|
||||
padding: 16px;
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.kanban-column {
|
||||
flex: 0 0 300px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
.kanban-column {
|
||||
flex: 0 0 300px;
|
||||
background: var(--mint-row-hover, #f1f5f9);
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
border-radius: var(--mint-radius, 10px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.column-header {
|
||||
padding: 14px 16px;
|
||||
background: var(--mint-surface, #fff);
|
||||
border-radius: 10px 10px 0 0;
|
||||
border-bottom: 1px solid var(--mint-border, #d9e8e2);
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: fit-content;
|
||||
min-height: 400px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.column-header {
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
.stage-name {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #1e293b);
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
.header-amount {
|
||||
font-size: 14px;
|
||||
color: var(--primary-dark, #059669);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.stage-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
.column-content {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-amount {
|
||||
font-size: 14px;
|
||||
color: #67c23a;
|
||||
font-weight: 600;
|
||||
}
|
||||
.opportunity-card {
|
||||
background: var(--mint-surface, #fff);
|
||||
border: 1px solid var(--mint-border, #d9e8e2);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--mint-shadow, 0 1px 3px rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--mint-text, #1e293b);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.column-content {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
.card-more {
|
||||
font-size: 16px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
cursor: pointer;
|
||||
|
||||
.opportunity-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-more {
|
||||
font-size: 16px;
|
||||
color: #1e293b;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&.amount {
|
||||
color: #67c23a;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
:deep(.el-progress) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.win-rate {
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
&:hover {
|
||||
color: var(--primary, #10b981);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
|
||||
.el-icon {
|
||||
font-size: 14px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
}
|
||||
|
||||
&.amount {
|
||||
color: var(--primary-dark, #059669);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
|
||||
:deep(.el-progress) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.win-rate {
|
||||
font-size: 12px;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2>产品分类</h2>
|
||||
<h2 class="title-bar-heading">产品分类</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
@@ -33,10 +33,10 @@
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按分类名称模糊查询。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -82,6 +82,8 @@
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions :loading="loading" @refresh="getList" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -220,6 +222,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { ref, reactive, onMounted, nextTick, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2>产品管理</h2>
|
||||
<h2 class="title-bar-heading">产品管理</h2>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区 -->
|
||||
@@ -43,10 +43,10 @@
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按产品名称、编号模糊查询。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>搜索
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -64,6 +64,24 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增产品
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="selectedRows.length === 0 || !allRowsDeletableByCreator(selectedRows)"
|
||||
:title="allRowsDeletableByCreator(selectedRows) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@@ -88,6 +106,16 @@
|
||||
<el-icon><Download /></el-icon>
|
||||
导出
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions
|
||||
:loading="loading"
|
||||
:column-defs="listColumnDefs"
|
||||
v-model:visible-columns="visibleColumns"
|
||||
v-model:check-all="checkAll"
|
||||
:is-indeterminate="isIndeterminate"
|
||||
@refresh="getList"
|
||||
@reset-columns="resetToDefault"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -100,9 +128,11 @@
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('productNo')"
|
||||
prop="productNo"
|
||||
label="产品编号"
|
||||
min-width="130"
|
||||
@@ -140,6 +170,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('categoryName')"
|
||||
prop="categoryName"
|
||||
label="产品分类"
|
||||
min-width="120"
|
||||
@@ -148,6 +179,7 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('model')"
|
||||
prop="model"
|
||||
label="规格型号"
|
||||
min-width="120"
|
||||
@@ -156,6 +188,7 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('unit')"
|
||||
prop="unit"
|
||||
label="单位"
|
||||
min-width="80"
|
||||
@@ -167,6 +200,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('price')"
|
||||
prop="price"
|
||||
label="标准价(元)"
|
||||
min-width="120"
|
||||
@@ -178,6 +212,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('cost')"
|
||||
prop="cost"
|
||||
label="成本价(元)"
|
||||
min-width="120"
|
||||
@@ -189,6 +224,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('status')"
|
||||
prop="status"
|
||||
label="状态"
|
||||
min-width="80"
|
||||
@@ -204,36 +240,6 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="160"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button class="btn-op-item primary" @click="handleUpdate(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-dropdown trigger="hover" @command="(cmd: string) => handleMoreCommand(cmd, row)">
|
||||
<el-button class="btn-op-item">
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="view">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided :disabled="!canDeleteByCreator(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -403,6 +409,22 @@
|
||||
modal-class="order-module-dialog-mask"
|
||||
append-to-body
|
||||
>
|
||||
<template #hero-actions>
|
||||
<div class="product-view-hero-actions">
|
||||
<el-button class="btn-main" type="primary" @click="handleDetailEdit">
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="!canDeleteByCreator(detailData)"
|
||||
:title="canDeleteByCreator(detailData) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDetailDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #images>
|
||||
<div v-if="productImageList.length" class="product-images">
|
||||
<ImagePreview
|
||||
@@ -470,6 +492,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, UploadFile } from 'element-plus'
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Close,
|
||||
Check,
|
||||
@@ -477,8 +500,6 @@ import {
|
||||
RefreshLeft,
|
||||
Download,
|
||||
Upload,
|
||||
View,
|
||||
More,
|
||||
Top,
|
||||
Bottom,
|
||||
Box
|
||||
@@ -490,8 +511,15 @@ import SystemViewDialog, {
|
||||
import { crmDictField, crmField, crmLongField, crmTagField } from '@/utils/crm/view-fields'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
import { formatMoneyDisplay } from '@/utils/money-format'
|
||||
import { canDeleteByCreator, filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import {
|
||||
canDeleteByCreator,
|
||||
filterDeletableByCreator,
|
||||
allRowsDeletableByCreator,
|
||||
CREATOR_DELETE_TIP
|
||||
} from '@/utils/creator-delete'
|
||||
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import {
|
||||
fetchCategoryTree,
|
||||
fetchProductList,
|
||||
@@ -519,8 +547,27 @@ function getHazardFlagTagType(value: unknown) {
|
||||
const productList = ref<any[]>([])
|
||||
const categoryOptions = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'productNo', label: '产品编号' },
|
||||
{ prop: 'categoryName', label: '产品分类' },
|
||||
{ prop: 'model', label: '规格型号' },
|
||||
{ prop: 'unit', label: '单位' },
|
||||
{ prop: 'price', label: '标准价(元)' },
|
||||
{ prop: 'cost', label: '成本价(元)', defaultVisible: false },
|
||||
{ prop: 'status', label: '状态' }
|
||||
]
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('crm-product-list-columns', listColumnDefs)
|
||||
|
||||
const selectedRows = ref<any[]>([])
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
@@ -691,6 +738,25 @@ function handleAdd() {
|
||||
formDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
ElMessage.warning('请选择一条数据进行编辑')
|
||||
return
|
||||
}
|
||||
handleUpdate(selectedRows.value[0])
|
||||
}
|
||||
|
||||
async function handleDetailEdit() {
|
||||
if (!detailData.value) return
|
||||
await handleUpdate(detailData.value)
|
||||
}
|
||||
|
||||
async function handleDetailDelete() {
|
||||
if (!detailData.value) return
|
||||
await handleDelete(detailData.value)
|
||||
detailDialogVisible.value = false
|
||||
}
|
||||
|
||||
async function handleUpdate(row: any) {
|
||||
isEdit.value = true
|
||||
resetFormData()
|
||||
@@ -726,11 +792,6 @@ async function handleView(row: any) {
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleMoreCommand(cmd: string, row: any) {
|
||||
if (cmd === 'view') handleView(row)
|
||||
else if (cmd === 'delete') handleDelete(row)
|
||||
}
|
||||
|
||||
async function handleDelete(row?: any) {
|
||||
if (!row && selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条数据进行删除')
|
||||
@@ -816,6 +877,10 @@ async function submitForm() {
|
||||
}
|
||||
formDialogVisible.value = false
|
||||
await getList()
|
||||
if (detailDialogVisible.value && formData.id) {
|
||||
const res = await fetchProductDetail({ id: formData.id })
|
||||
detailData.value = (res as any)?.data || res || null
|
||||
}
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
@@ -958,4 +1023,12 @@ onMounted(() => {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.product-view-hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -171,7 +171,7 @@ watch(dialogVisible, (val) => {
|
||||
// 查询参数
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
productName: undefined,
|
||||
categoryId: undefined,
|
||||
status: 'on' // on-上架 off-下架
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<p v-if="quote?.quoteNo" class="detail-subtitle">{{ quote.quoteNo }} — {{ quote.quoteTitle }}</p>
|
||||
|
||||
<el-tabs v-model="activeTab" class="panel-tabs">
|
||||
<el-tabs v-model="activeTab" class="panel-pill-tabs panel-pill-tabs--lg">
|
||||
<!-- 基本信息 -->
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
<el-descriptions :column="2" border>
|
||||
|
||||
@@ -5,14 +5,16 @@
|
||||
<h2 class="title-bar-heading">报价单管理</h2>
|
||||
</div>
|
||||
|
||||
<div class="panel-tabs">
|
||||
<el-tabs v-model="activeView" @tab-change="handleViewChange">
|
||||
<el-tab-pane label="我的报价" name="my" />
|
||||
<el-tab-pane label="全部报价" name="all" />
|
||||
<el-tab-pane label="待审批" name="submitted" />
|
||||
<el-tab-pane label="已审批" name="accepted" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
<el-tabs
|
||||
v-model="activeView"
|
||||
class="panel-pill-tabs panel-pill-tabs--lg"
|
||||
@tab-change="handleViewChange"
|
||||
>
|
||||
<el-tab-pane label="我的报价" name="my" />
|
||||
<el-tab-pane label="全部报价" name="all" />
|
||||
<el-tab-pane label="待审批" name="submitted" />
|
||||
<el-tab-pane label="已审批" name="accepted" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form ref="queryFormRef" :model="queryParams" class="filter-form" @submit.prevent>
|
||||
@@ -139,6 +141,24 @@
|
||||
<el-button v-if="hasQuotePerm('add')" class="btn-main" type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>新增
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasQuotePerm('edit')"
|
||||
class="btn-ghost"
|
||||
:disabled="single || selectedRows[0]?.status !== 'draft'"
|
||||
@click="handleUpdate()"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasQuotePerm('remove')"
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="single || selectedRows[0]?.status !== 'draft' || !canDeleteByCreator(selectedRows[0])"
|
||||
:title="canDeleteByCreator(selectedRows[0]) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
<el-button v-if="hasQuotePerm('add')" class="btn-ghost" :disabled="single" @click="handleCopyAdd()">
|
||||
<el-icon><DocumentCopy /></el-icon>复制新增
|
||||
</el-button>
|
||||
@@ -164,12 +184,16 @@
|
||||
<el-button v-if="hasQuotePerm('edit')" class="btn-ghost" :disabled="multiple" @click="handleInvalid()">
|
||||
<el-icon><CircleClose /></el-icon>作废
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" @click="handleColumnSetting">
|
||||
<el-icon><Setting /></el-icon>列设置
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="loading" @click="getList">
|
||||
<el-icon><Refresh /></el-icon>刷新
|
||||
</el-button>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -399,23 +423,6 @@
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column v-if="isColumnVisible('remark')" label="备注" align="center" prop="remark" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="操作" min-width="260" align="center" header-align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; justify-content: center; gap: 8px">
|
||||
<el-button v-if="hasQuotePerm('query')" class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>查看
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="hasQuotePerm('edit')"
|
||||
class="btn-op-item primary"
|
||||
:disabled="row.status !== 'draft'"
|
||||
@click="handleUpdate(row)"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>编辑
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -714,38 +721,6 @@
|
||||
@edit="handleEditFromDetail"
|
||||
/>
|
||||
|
||||
<!-- 列设置对话框 -->
|
||||
<el-dialog
|
||||
v-model="columnSettingDialog.visible"
|
||||
title="列设置"
|
||||
width="600px"
|
||||
align-center
|
||||
append-to-body
|
||||
class="dialog-form-layout"
|
||||
modal-class="order-module-dialog-mask"
|
||||
>
|
||||
<div class="column-setting-content">
|
||||
<el-checkbox v-model="checkAll" :indeterminate="isIndeterminate" @change="handleCheckAllChange">全选</el-checkbox>
|
||||
<el-divider />
|
||||
<el-checkbox-group v-model="visibleColumns" @change="handleCheckedColumnsChange">
|
||||
<div v-for="col in tableColumns" :key="col.prop" class="column-item">
|
||||
<el-checkbox :label="col.prop" :disabled="col.fixed">{{ col.label }}</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="handleResetColumns">
|
||||
<el-icon><RefreshLeft /></el-icon>恢复默认
|
||||
</el-button>
|
||||
<el-button @click="columnSettingDialog.visible = false">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleSaveColumnSetting">
|
||||
<el-icon><Check /></el-icon>保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<QuoteFormDialog ref="quoteFormDialogRef" @success="getList" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -767,12 +742,9 @@ import {
|
||||
Edit,
|
||||
Plus,
|
||||
Printer,
|
||||
Refresh,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
Select,
|
||||
Setting,
|
||||
View
|
||||
Select
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useDictStore } from '@/store/modules/dict'
|
||||
const dictStore = useDictStore()
|
||||
@@ -816,9 +788,11 @@ import html2canvas from 'html2canvas';
|
||||
import jsPDF from 'jspdf';
|
||||
|
||||
import { useQuotePerm } from '@/hooks/business/useQuotePerm'
|
||||
import { filterDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import { filterDeletableByCreator, canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import QuoteFormDialog from './components/QuoteFormDialog.vue'
|
||||
import QuoteDetailDialog from './components/QuoteDetailDialog.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
|
||||
const { loadPerms, hasQuotePerm } = useQuotePerm()
|
||||
const router = useRouter()
|
||||
@@ -874,117 +848,55 @@ const getApprovalTimelineColor = (result?: string) => {
|
||||
return '#94a3b8';
|
||||
};
|
||||
|
||||
// 列配置相关
|
||||
const COLUMN_STORAGE_KEY = 'crm_quote_visible_columns';
|
||||
// 列配置
|
||||
const listColumnDefs = [
|
||||
{ prop: 'quoteType', label: '报价类型' },
|
||||
{ prop: 'customerName', label: '客户名称' },
|
||||
{ prop: 'opportunityName', label: '商机名称' },
|
||||
{ prop: 'contactName', label: '联系人' },
|
||||
{ prop: 'contactMobile', label: '联系人手机' },
|
||||
{ prop: 'contactEmail', label: '联系人邮箱' },
|
||||
{ prop: 'version', label: '版本号' },
|
||||
{ prop: 'totalAmount', label: '总金额' },
|
||||
{ prop: 'discountRate', label: '折扣率' },
|
||||
{ prop: 'discountAmount', label: '折扣金额' },
|
||||
{ prop: 'finalAmount', label: '最终金额' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'validityDays', label: '有效期' },
|
||||
{ prop: 'quoteDate', label: '报价日期' },
|
||||
{ prop: 'expireDate', label: '到期日期' },
|
||||
{ prop: 'rejectReason', label: '拒绝原因' },
|
||||
{ prop: 'viewCount', label: '查看次数' },
|
||||
{ prop: 'lastViewTime', label: '最后查看时间' },
|
||||
{ prop: 'ownerUserName', label: '负责人' },
|
||||
{ prop: 'paymentTerms', label: '付款条款' },
|
||||
{ prop: 'deliveryTerms', label: '交付条款' },
|
||||
{ prop: 'createDeptName', label: '创建部门' },
|
||||
{ prop: 'createByName', label: '创建人' },
|
||||
{ prop: 'createTime', label: '创建时间' },
|
||||
{ prop: 'updateByName', label: '更新人' },
|
||||
{ prop: 'updateTime', label: '更新时间' },
|
||||
{ prop: 'remark', label: '备注' }
|
||||
]
|
||||
|
||||
// 定义所有列配置(顺序代表显示顺序)
|
||||
const tableColumns = ref([
|
||||
{ prop: 'quoteType', label: '报价类型', fixed: false },
|
||||
{ prop: 'customerName', label: '客户名称', fixed: false },
|
||||
{ prop: 'opportunityName', label: '商机名称', fixed: false },
|
||||
{ prop: 'contactName', label: '联系人', fixed: false },
|
||||
{ prop: 'contactMobile', label: '联系人手机', fixed: false },
|
||||
{ prop: 'contactEmail', label: '联系人邮箱', fixed: false },
|
||||
{ prop: 'version', label: '版本号', fixed: false },
|
||||
{ prop: 'totalAmount', label: '总金额', fixed: false },
|
||||
{ prop: 'discountRate', label: '折扣率', fixed: false },
|
||||
{ prop: 'discountAmount', label: '折扣金额', fixed: false },
|
||||
{ prop: 'finalAmount', label: '最终金额', fixed: false },
|
||||
{ prop: 'status', label: '状态', fixed: false },
|
||||
{ prop: 'validityDays', label: '有效期', fixed: false },
|
||||
{ prop: 'quoteDate', label: '报价日期', fixed: false },
|
||||
{ prop: 'expireDate', label: '到期日期', fixed: false },
|
||||
{ prop: 'rejectReason', label: '拒绝原因', fixed: false },
|
||||
{ prop: 'viewCount', label: '查看次数', fixed: false },
|
||||
{ prop: 'lastViewTime', label: '最后查看时间', fixed: false },
|
||||
{ prop: 'ownerUserName', label: '负责人', fixed: false },
|
||||
{ prop: 'paymentTerms', label: '付款条款', fixed: false },
|
||||
{ prop: 'deliveryTerms', label: '交付条款', fixed: false },
|
||||
{ prop: 'createDeptName', label: '创建部门', fixed: false },
|
||||
{ prop: 'createByName', label: '创建人', fixed: false },
|
||||
{ prop: 'createTime', label: '创建时间', fixed: false },
|
||||
{ prop: 'updateByName', label: '更新人', fixed: false },
|
||||
{ prop: 'updateTime', label: '更新时间', fixed: false },
|
||||
{ prop: 'remark', label: '备注', fixed: false }
|
||||
]);
|
||||
|
||||
// 默认显示的列(现有字段)
|
||||
const defaultVisibleColumns = ['quoteType', 'customerName', 'finalAmount', 'status', 'validityDays', 'ownerUserName', 'quoteDate', 'createTime'];
|
||||
|
||||
// 当前显示的列
|
||||
const visibleColumns = ref<string[]>([]);
|
||||
|
||||
// 列设置对话框
|
||||
const columnSettingDialog = reactive({
|
||||
visible: false
|
||||
});
|
||||
|
||||
// 全选状态
|
||||
const checkAll = ref(false);
|
||||
const isIndeterminate = ref(false);
|
||||
|
||||
/** 初始化列配置 */
|
||||
const initColumnSetting = () => {
|
||||
const savedColumns = localStorage.getItem(COLUMN_STORAGE_KEY);
|
||||
if (savedColumns) {
|
||||
try {
|
||||
visibleColumns.value = JSON.parse(savedColumns);
|
||||
} catch (error) {
|
||||
console.error('解析列配置失败:', error);
|
||||
visibleColumns.value = [...defaultVisibleColumns];
|
||||
}
|
||||
} else {
|
||||
visibleColumns.value = [...defaultVisibleColumns];
|
||||
}
|
||||
updateCheckAllStatus();
|
||||
};
|
||||
|
||||
/** 判断列是否显示 */
|
||||
const isColumnVisible = (prop: string) => {
|
||||
return visibleColumns.value.includes(prop);
|
||||
};
|
||||
|
||||
/** 打开列设置对话框 */
|
||||
const handleColumnSetting = () => {
|
||||
columnSettingDialog.visible = true;
|
||||
};
|
||||
|
||||
/** 全选/取消全选 */
|
||||
const handleCheckAllChange = (val: boolean) => {
|
||||
visibleColumns.value = val ? tableColumns.value.map((col) => col.prop) : [];
|
||||
isIndeterminate.value = false;
|
||||
};
|
||||
|
||||
/** 列选择变化 */
|
||||
const handleCheckedColumnsChange = () => {
|
||||
updateCheckAllStatus();
|
||||
};
|
||||
|
||||
/** 更新全选状态 */
|
||||
const updateCheckAllStatus = () => {
|
||||
const checkedCount = visibleColumns.value.length;
|
||||
const totalCount = tableColumns.value.length;
|
||||
checkAll.value = checkedCount === totalCount;
|
||||
isIndeterminate.value = checkedCount > 0 && checkedCount < totalCount;
|
||||
};
|
||||
|
||||
/** 恢复默认列设置 */
|
||||
const handleResetColumns = () => {
|
||||
visibleColumns.value = [...defaultVisibleColumns];
|
||||
updateCheckAllStatus();
|
||||
};
|
||||
|
||||
/** 保存列设置 */
|
||||
const handleSaveColumnSetting = () => {
|
||||
try {
|
||||
localStorage.setItem(COLUMN_STORAGE_KEY, JSON.stringify(visibleColumns.value));
|
||||
$modal.msgSuccess('列设置保存成功');
|
||||
columnSettingDialog.visible = false;
|
||||
} catch (error) {
|
||||
console.error('保存列配置失败:', error);
|
||||
$modal.msgError('保存列设置失败');
|
||||
}
|
||||
};
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('crm_quote_visible_columns', listColumnDefs, {
|
||||
defaultVisibleProps: [
|
||||
'quoteType',
|
||||
'customerName',
|
||||
'finalAmount',
|
||||
'status',
|
||||
'validityDays',
|
||||
'ownerUserName',
|
||||
'quoteDate',
|
||||
'createTime'
|
||||
]
|
||||
})
|
||||
|
||||
type QuoteViewName = 'my' | 'all' | 'submitted' | 'accepted'
|
||||
|
||||
@@ -998,7 +910,7 @@ interface TabFilterState {
|
||||
const createDefaultTabFilter = (): TabFilterState => ({
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
keyword: undefined,
|
||||
quoteNo: undefined,
|
||||
quoteTitle: undefined,
|
||||
@@ -1023,7 +935,7 @@ const tabFilterMap = reactive<Record<QuoteViewName, TabFilterState>>({
|
||||
// 查询参数(当前 tab 的检索条件)
|
||||
const queryParams = ref<QuoteQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
keyword: undefined,
|
||||
quoteNo: undefined,
|
||||
quoteTitle: undefined,
|
||||
@@ -2514,7 +2426,6 @@ watchDebounced(
|
||||
|
||||
onMounted(() => {
|
||||
loadPerms()
|
||||
initColumnSetting();
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -2780,25 +2691,6 @@ onMounted(() => {
|
||||
min-height: 32px !important;
|
||||
}
|
||||
|
||||
/* 列设置对话框样式 */
|
||||
.column-setting-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
.column-item {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quote-product-expand {
|
||||
padding: 12px 16px;
|
||||
background-color: var(--mint-table-head, #f8fafc);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<!-- 精简工作台:无业务统计接口,仅作入口与 UI 规范说明 -->
|
||||
<!-- Demo 工作台:Fresh Mint 模板页入口 -->
|
||||
<template>
|
||||
<div class="console-container console-lite">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2>快速入口</h2>
|
||||
<h2 class="title-bar-heading">Demo 模板页</h2>
|
||||
</div>
|
||||
<div class="console-lite__body">
|
||||
<p class="console-lite__hint"
|
||||
>详见仓库根目录 `.cursorrules` 中「页面级 UI 模板(固定五页)」。</p
|
||||
>
|
||||
<p class="console-lite__hint">
|
||||
以下页面为 Fresh Mint UI 参考模板,数据为本地 Mock,不影响业务模块。详见
|
||||
<code>docs/web-admin-ui-spec-fresh-mint.md</code> 与
|
||||
<code>docs/web-admin-ui-pattern-index.md</code>。
|
||||
</p>
|
||||
<div class="console-lite__links">
|
||||
<el-button
|
||||
v-for="item in links"
|
||||
@@ -17,6 +19,7 @@
|
||||
type="primary"
|
||||
@click="go(item.path)"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
{{ item.label }}
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -27,17 +30,18 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Link } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({ name: 'Console' })
|
||||
defineOptions({ name: 'DemoConsole' })
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const links = [
|
||||
{ label: '商品单位', path: '/system/unit' },
|
||||
{ label: '用户管理', path: '/system/user' },
|
||||
{ label: '商品分类', path: '/system/category' },
|
||||
{ label: '租户管理', path: '/system/tenant' },
|
||||
{ label: '客户管理', path: '/basic-info/customer' }
|
||||
{ label: '标准列表(单位)', path: '/demo/unit' },
|
||||
{ label: '复杂列表(订单)', path: '/demo/order' },
|
||||
{ label: '商品列表', path: '/demo/product' },
|
||||
{ label: '树形主数据(分类)', path: '/demo/category' },
|
||||
{ label: '用户 CRUD', path: '/demo/user' }
|
||||
]
|
||||
|
||||
const go = (path: string) => {
|
||||
@@ -54,12 +58,22 @@
|
||||
margin: 0 0 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #1e293b;
|
||||
color: var(--mint-text-muted, #64748b);
|
||||
|
||||
code {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.console-lite__links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.el-button {
|
||||
:deep(.el-icon) {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -139,14 +139,65 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增记录
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length !== 1"
|
||||
@click="handleToolbarEdit"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedRows.length === 1 && selectedRows[0]?.purchasePlanId"
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows[0]?.hasWeighingSlip"
|
||||
@click="handleGenerateWeighing(selectedRows[0])"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
{{ selectedRows[0]?.hasWeighingSlip ? '已生成地磅单' : '生成地磅单' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="selectedRows.length === 1 && selectedRows[0]?.purchasePlanId"
|
||||
class="btn-ghost"
|
||||
@click="handleCopyFillLink(selectedRows[0])"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
磅单二维码
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="
|
||||
selectedRows.length !== 1 ||
|
||||
!canDeleteRecord(selectedRows[0]) ||
|
||||
!canDeleteByCreator(selectedRows[0])
|
||||
"
|
||||
:title="
|
||||
selectedRows.length === 1 && !canDeleteRecord(selectedRows[0])
|
||||
? '仅支持删除当天及之后日期的记录'
|
||||
: selectedRows.length === 1 && !canDeleteByCreator(selectedRows[0])
|
||||
? CREATOR_DELETE_TIP
|
||||
: ''
|
||||
"
|
||||
@click="handleToolbarDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="exportLoading" @click="handleExport">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出 Excel
|
||||
</el-button>
|
||||
<el-button class="btn-ghost" :loading="loading" @click="loadData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
|
||||
<ListToolbarEndActions
|
||||
:loading="loading"
|
||||
:column-defs="listColumnDefs"
|
||||
v-model:visible-columns="visibleColumns"
|
||||
v-model:check-all="checkAll"
|
||||
:is-indeterminate="isIndeterminate"
|
||||
@refresh="loadData"
|
||||
@reset-columns="resetToDefault"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -161,10 +212,12 @@
|
||||
:summary-method="getSummary"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-dblclick="handleView"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('id')"
|
||||
prop="id"
|
||||
label="ID"
|
||||
min-width="100"
|
||||
@@ -175,6 +228,7 @@
|
||||
:sort-order="getSortOrder('id')"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('planNo')"
|
||||
prop="planNo"
|
||||
label="计划号"
|
||||
min-width="140"
|
||||
@@ -189,8 +243,13 @@
|
||||
show-overflow-tooltip
|
||||
sortable="custom"
|
||||
:sort-order="getSortOrder('batchNo')"
|
||||
/>
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-link type="primary" @click="handleView(row)">{{ row.batchNo }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('date')"
|
||||
prop="date"
|
||||
label="日期"
|
||||
min-width="120"
|
||||
@@ -198,6 +257,7 @@
|
||||
:sort-order="getSortOrder('date')"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('partnerCompany')"
|
||||
prop="partnerCompany"
|
||||
label="往来单位"
|
||||
min-width="150"
|
||||
@@ -206,6 +266,7 @@
|
||||
:sort-order="getSortOrder('partnerCompany')"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('businessType')"
|
||||
prop="businessType"
|
||||
label="业务类型"
|
||||
min-width="100"
|
||||
@@ -219,6 +280,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('status')"
|
||||
prop="status"
|
||||
label="状态"
|
||||
min-width="110"
|
||||
@@ -232,6 +294,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('product')"
|
||||
prop="product"
|
||||
label="产品"
|
||||
min-width="120"
|
||||
@@ -240,6 +303,7 @@
|
||||
:sort-order="getSortOrder('product')"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('specification')"
|
||||
prop="specification"
|
||||
label="规格"
|
||||
min-width="120"
|
||||
@@ -248,6 +312,7 @@
|
||||
:sort-order="getSortOrder('specification')"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('vehicleNo')"
|
||||
prop="vehicleNo"
|
||||
label="车牌号"
|
||||
min-width="120"
|
||||
@@ -255,6 +320,7 @@
|
||||
:sort-order="getSortOrder('vehicleNo')"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('quantity')"
|
||||
prop="quantity"
|
||||
label="数量(吨)"
|
||||
min-width="110"
|
||||
@@ -262,6 +328,7 @@
|
||||
:sort-order="getSortOrder('quantity')"
|
||||
/>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('netWeight')"
|
||||
prop="netWeight"
|
||||
label="净重(吨)"
|
||||
min-width="110"
|
||||
@@ -273,6 +340,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('grossWeight')"
|
||||
prop="grossWeight"
|
||||
label="毛重(吨)"
|
||||
min-width="110"
|
||||
@@ -284,6 +352,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('grossWeightTime')"
|
||||
prop="grossWeightTime"
|
||||
label="毛重时间"
|
||||
min-width="160"
|
||||
@@ -295,6 +364,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('tareWeight')"
|
||||
prop="tareWeight"
|
||||
label="皮重(吨)"
|
||||
min-width="110"
|
||||
@@ -306,6 +376,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('tareWeightTime')"
|
||||
prop="tareWeightTime"
|
||||
label="皮重时间"
|
||||
min-width="160"
|
||||
@@ -317,6 +388,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('deduction')"
|
||||
prop="deduction"
|
||||
label="扣杂(吨)"
|
||||
min-width="110"
|
||||
@@ -328,6 +400,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="isColumnVisible('remarks')"
|
||||
prop="remarks"
|
||||
label="备注"
|
||||
min-width="150"
|
||||
@@ -335,52 +408,6 @@
|
||||
sortable="custom"
|
||||
:sort-order="getSortOrder('remarks')"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
min-width="520"
|
||||
align="center"
|
||||
header-align="center"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="db-op-actions">
|
||||
<el-button class="btn-op-item" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.purchasePlanId"
|
||||
class="btn-op-item"
|
||||
:disabled="row.hasWeighingSlip"
|
||||
@click="handleGenerateWeighing(row)"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
{{ row.hasWeighingSlip ? '已生成地磅单' : '生成地磅单' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.purchasePlanId"
|
||||
class="btn-op-item"
|
||||
@click="handleCopyFillLink(row)"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
磅单二维码
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item danger"
|
||||
:disabled="!canDeleteRecord(row) || !canDeleteByCreator(row)"
|
||||
:title="!canDeleteRecord(row) ? '仅支持删除当天及之后日期的记录' : canDeleteByCreator(row) ? '' : CREATOR_DELETE_TIP"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
@@ -485,7 +512,47 @@
|
||||
v-model:visible="relatedModalOpen"
|
||||
:loading="loadingRelated"
|
||||
:data="relatedData"
|
||||
/>
|
||||
>
|
||||
<template v-if="viewingRecord" #hero-actions>
|
||||
<el-button class="btn-main" type="primary" @click="handleEdit(viewingRecord)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="viewingRecord.purchasePlanId"
|
||||
class="btn-ghost"
|
||||
:disabled="viewingRecord.hasWeighingSlip"
|
||||
@click="handleGenerateWeighing(viewingRecord)"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
{{ viewingRecord.hasWeighingSlip ? '已生成地磅单' : '生成地磅单' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="viewingRecord.purchasePlanId"
|
||||
class="btn-ghost"
|
||||
@click="handleCopyFillLink(viewingRecord)"
|
||||
>
|
||||
<el-icon><Link /></el-icon>
|
||||
磅单二维码
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
type="danger"
|
||||
:disabled="!canDeleteRecord(viewingRecord) || !canDeleteByCreator(viewingRecord)"
|
||||
:title="
|
||||
!canDeleteRecord(viewingRecord)
|
||||
? '仅支持删除当天及之后日期的记录'
|
||||
: canDeleteByCreator(viewingRecord)
|
||||
? ''
|
||||
: CREATOR_DELETE_TIP
|
||||
"
|
||||
@click="handleViewDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</DbInventoryRelatedViewDialog>
|
||||
|
||||
<DbPoundSlipQrcodeDialog v-model:visible="qrcodeModalOpen" :inventory-id="qrcodeInventoryId" />
|
||||
</div>
|
||||
@@ -508,12 +575,12 @@
|
||||
Edit,
|
||||
Link,
|
||||
Plus,
|
||||
Refresh,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import DbCrmCustomerSelect from '@/components/business/db/DbCrmCustomerSelect.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
|
||||
import { canDeleteByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
|
||||
import DbCrmProductSelect from '@/components/business/db/DbCrmProductSelect.vue'
|
||||
import DbInventoryRelatedViewDialog from '@/components/business/db/DbInventoryRelatedViewDialog.vue'
|
||||
@@ -535,13 +602,42 @@
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const listColumnDefs = [
|
||||
{ prop: 'id', label: 'ID', defaultVisible: false },
|
||||
{ prop: 'planNo', label: '计划号' },
|
||||
{ prop: 'date', label: '日期' },
|
||||
{ prop: 'partnerCompany', label: '往来单位' },
|
||||
{ prop: 'businessType', label: '业务类型' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'product', label: '产品' },
|
||||
{ prop: 'specification', label: '规格' },
|
||||
{ prop: 'vehicleNo', label: '车牌号' },
|
||||
{ prop: 'quantity', label: '数量(吨)' },
|
||||
{ prop: 'netWeight', label: '净重(吨)' },
|
||||
{ prop: 'grossWeight', label: '毛重(吨)' },
|
||||
{ prop: 'grossWeightTime', label: '毛重时间' },
|
||||
{ prop: 'tareWeight', label: '皮重(吨)' },
|
||||
{ prop: 'tareWeightTime', label: '皮重时间' },
|
||||
{ prop: 'deduction', label: '扣杂(吨)' },
|
||||
{ prop: 'remarks', label: '备注' }
|
||||
]
|
||||
|
||||
const {
|
||||
visibleColumns,
|
||||
checkAll,
|
||||
isIndeterminate,
|
||||
isColumnVisible,
|
||||
resetToDefault
|
||||
} = usePersistedListColumns('db-inventory-list-columns', listColumnDefs)
|
||||
|
||||
const exportLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
const tableData = ref<InventoryRecord[]>([])
|
||||
const allRecords = ref<InventoryRecord[]>([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const pageSize = ref(20)
|
||||
const activeDateShortcut = ref<'all' | 'today' | 'tomorrow' | 'yesterday' | 'week' | ''>('today')
|
||||
const searchKeyword = ref('')
|
||||
const selectedRows = ref<InventoryRecord[]>([])
|
||||
@@ -559,6 +655,7 @@
|
||||
const relatedModalOpen = ref(false)
|
||||
const loadingRelated = ref(false)
|
||||
const relatedData = ref<InventoryRelatedData>({ plan: null, weighing: null })
|
||||
const viewingRecord = ref<InventoryRecord | null>(null)
|
||||
|
||||
const filters = reactive({
|
||||
batchNo: '',
|
||||
@@ -845,6 +942,7 @@
|
||||
}
|
||||
|
||||
const handleView = async (record: InventoryRecord) => {
|
||||
viewingRecord.value = record
|
||||
loadingRelated.value = true
|
||||
try {
|
||||
relatedData.value = await inventoryApi.getRelated(record.id)
|
||||
@@ -856,6 +954,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleToolbarEdit() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
$modal.msgWarning('请先勾选一条记录')
|
||||
return
|
||||
}
|
||||
handleEdit(selectedRows.value[0])
|
||||
}
|
||||
|
||||
function handleToolbarDelete() {
|
||||
if (selectedRows.value.length !== 1) {
|
||||
$modal.msgWarning('请先勾选一条记录')
|
||||
return
|
||||
}
|
||||
handleDelete(selectedRows.value[0])
|
||||
}
|
||||
|
||||
async function handleViewDelete() {
|
||||
if (!viewingRecord.value) return
|
||||
await handleDelete(viewingRecord.value)
|
||||
relatedModalOpen.value = false
|
||||
}
|
||||
|
||||
const handleEdit = (record: InventoryRecord) => {
|
||||
editingRecord.value = record
|
||||
Object.assign(formData, {
|
||||
|
||||
Loaded 100 of 249 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user