添加 采集 修改车辆信息页面 备货单页面调整 审核菜单徽标

This commit is contained in:
浅念
2025-10-12 17:14:32 +08:00
parent dee800f90f
commit 0b89bade20
35 changed files with 5848 additions and 543 deletions
+17
View File
@@ -152,6 +152,17 @@
// 监听ws消息,用于系统级通知
mittBus.on('ws:message', handleWsMessageNotification)
// 初始化菜单徽标系统(全局初始化,避免重复)
import('@/composables/useMenuBadge').then(({ useMenuBadge }) => {
const { initBadgeSystem } = useMenuBadge()
initBadgeSystem()
})
// 加载菜单徽标测试工具(仅开发环境)
if (import.meta.env.DEV) {
import('@/utils/menuBadgeTest')
}
})
// 登录后,accessToken 变更时立即初始化 WebSocket(无需刷新)
@@ -166,6 +177,12 @@
) {
initWebSocket(token)
}
// 重新初始化徽标系统(用户重新登录时)
import('@/composables/useMenuBadge').then(({ useMenuBadge }) => {
const { initBadgeSystem } = useMenuBadge()
initBadgeSystem()
})
} else {
// 退出登录时,关闭并重置
const { closeWebSocket, resetWebSocketReconnect } = await import('@/utils/sys/wsClient')
+32
View File
@@ -0,0 +1,32 @@
import request from '@/utils/http'
export class userConService {
//分组列表
static getGroupList(params: Api.Common.PaginatingParams) {
return request.get<Api.Common.CommonListData>({
url: '/admin/user/group/list',
params
})
}
//添加分组
static addGroup(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/user/group/store',
data: params
})
}
//编辑分组
static editGroup(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/user/group/update',
data: params
})
}
//删除分组
static deleteGroup(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/admin/user/group/destroy',
data: params
})
}
}
+20
View File
@@ -61,6 +61,13 @@ export class commonService {
params
})
}
//车型下拉列表
static getModelsSelect(params: any) {
return request.get<any>({
url: '/crm/vehicle/sku/preset',
params
})
}
//港口下拉列表
static getPortSelect(params: any) {
return request.get<any>({
@@ -132,4 +139,17 @@ export class commonService {
params
})
}
//人员分组场景
static getUserGroupScene() {
return request.get<any>({
url: '/common/preset/usergroup/scene'
})
}
//人员分组
static getUserGroupSelect(params: any) {
return request.get<any>({
url: '/common/preset/usergroup',
params
})
}
}
+35
View File
@@ -285,4 +285,39 @@ export class CrmService {
params
})
}
//备货单撤回
static stockrecall(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/crm/stock/order/revoke/approval',
params
})
}
//备货单作废
static stockinvalid(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/crm/stock/order/nullify/approval',
params
})
}
//备货单删除
static stockdelete(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/crm/stock/order/destroy',
params
})
}
//审核详情
static stockapprovaldetailborder(params: any) {
return request.get<any>({
url: '/crm/approval/records/border',
params
})
}
//审批转交
static stockapprovaltransfer(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/crm/approval/records/transfer',
params
})
}
}
+123
View File
@@ -95,4 +95,127 @@ export class vehicleService {
params
})
}
// 港口管理
static getPortList(params: Api.Common.PaginatingParams & any) {
return request.get<Api.Common.CommonListData>({
url: '/common/port/list',
params
})
}
static portAdd(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/port/store',
params
})
}
static portDetail(params: any) {
return request.get<any>({
url: '/common/port/show',
params
})
}
static portEdit(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/port/update',
params
})
}
static portDelete(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/port/destroy',
params
})
}
// 车辆品牌列表
static getBrandList(params: Api.Common.PaginatingParams & any) {
return request.get<Api.Common.CommonListData>({
url: '/common/brand/list',
params
})
}
// 车辆品牌新增/详情/编辑/删除
static brandAdd(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/store',
params
})
}
static brandDetail(params: any) {
return request.get<any>({
url: '/common/brand/show',
params
})
}
static brandEdit(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/update',
params
})
}
static brandDelete(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/destroy',
params
})
}
// 车辆系列列表
static getSeriesList(params: Api.Common.PaginatingParams & any) {
return request.get<Api.Common.CommonListData>({
url: '/common/brand/series/list',
params
})
}
// 车辆系列新增/详情/编辑/删除
static seriesAdd(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/series/store',
params
})
}
static seriesDetail(params: any) {
return request.get<any>({
url: '/common/brand/series/show',
params
})
}
static seriesEdit(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/series/update',
params
})
}
static seriesDelete(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/series/destroy',
params
})
}
// 车系采集相关接口
static collectVehicleSeries(params: any) {
return request.get<any>({
url: '/common/brand/crawlSeries',
params
})
}
static saveVehicleSeries(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/crawlSeriesCreate',
params
})
}
// 车型采集相关接口
static collectVehicleModels(params: any) {
return request.get<any>({
url: '/common/brand/series/crawlModels',
params
})
}
static saveVehicleModels(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/series/crawlModelCreate',
params
})
}
}
+28
View File
@@ -193,4 +193,32 @@ export class vehicleService {
params
})
}
// 车系采集相关接口
static collectVehicleSeries(params: any) {
return request.get<any>({
url: '/common/brand/crawlSeries',
params
})
}
static saveVehicleSeries(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/crawlSeriesCreate',
params
})
}
// 车型采集相关接口
static collectVehicleModels(params: any) {
return request.get<any>({
url: '/common/brand/series/crawlModels',
params
})
}
static saveVehicleModels(params: any) {
return request.post<Api.Common.AddResponse>({
url: '/common/brand/series/crawlModelCreate',
params
})
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
//
'white': #ffffff,
'black': #000000,
'success': ('base': #13deb9),
'success': ('base': #00A43F),
'warning': ('base': #ffae1f),
'danger': ('base': #ff4d4f),
'error': ('base': #fa896b)
+1 -1
View File
@@ -8,7 +8,7 @@
//
'white': #ffffff,
'black': #000000,
'success': ('base': #13deb9),
'success': ('base': #00A43F),
'warning': ('base': #ffae1f),
'danger': ('base': #ff4d4f),
'error': ('base': #fa896b)
@@ -32,7 +32,10 @@
v-html="item.meta.icon"
></i>
<span>{{ formatMenuTitle(item.meta.title) }}</span>
<div class="badge" v-if="item.meta.showBadge"></div>
<div v-if="isMenuBadgeVisible(item.path) && !getMenuTextBadge(item.path)" class="badge"></div>
<div v-if="getMenuTextBadge(item.path)" class="text-badge">
{{ getMenuTextBadge(item.path) }}
</div>
</el-menu-item>
</template>
@@ -41,6 +44,7 @@
import { AppRouteRecord } from '@/types/router'
import { handleMenuJump } from '@/utils/navigation'
import { formatMenuTitle } from '@/router/utils/utils'
import { useMenuBadge } from '@/composables/useMenuBadge'
const props = defineProps({
item: {
@@ -60,6 +64,9 @@
const emit = defineEmits(['close'])
// 使用菜单徽标系统
const { isMenuBadgeVisible, getMenuTextBadge } = useMenuBadge()
// 计算当前项是否有子菜单
const hasChildren = computed(() => {
return props.item.children && props.item.children.length > 0
@@ -112,16 +112,17 @@
top: 0;
right: 12px;
bottom: 0;
min-width: 20px;
height: 20px;
padding: 0 5px;
min-width: 16px;
height: 16px;
// padding: 0 5px;
margin: auto;
font-size: 12px;
line-height: 20px;
font-size: 10px;
line-height: 16px;
color: #fff;
text-align: center;
background: #fd4e4e;
border-radius: 5px;
// border-radius: 5px;
border-radius: 50%;
}
.header {
@@ -5,7 +5,10 @@
<template #title>
<MenuItemIcon :icon="item.meta.icon" :color="theme?.iconColor" />
<span class="menu-name">{{ formatMenuTitle(item.meta.title) }}</span>
<div v-if="item.meta.showBadge" class="badge" style="right: 35px" />
<div v-if="isMenuBadgeVisible(item.path) && !getMenuTextBadge(item.path)" class="badge" style="right: 35px" />
<div v-if="getMenuTextBadge(item.path)" class="text-badge" style="right: 35px">
{{ getMenuTextBadge(item.path) }}
</div>
</template>
<SidebarSubmenu
:list="item.children"
@@ -26,9 +29,9 @@
<MenuItemIcon :icon="item.meta.icon" :color="theme?.iconColor" />
<template #title>
<span class="menu-name">{{ formatMenuTitle(item.meta.title) }}</span>
<div v-if="item.meta.showBadge" class="badge" />
<div v-if="item.meta.showTextBadge" class="text-badge">
{{ item.meta.showTextBadge }}
<div v-if="isMenuBadgeVisible(item.path) && !getMenuTextBadge(item.path)" class="badge" />
<div v-if="getMenuTextBadge(item.path)" class="text-badge">
{{ getMenuTextBadge(item.path) }}
</div>
</template>
</el-menu-item>
@@ -40,6 +43,7 @@
import type { AppRouteRecord } from '@/types/router'
import { formatMenuTitle } from '@/router/utils/utils'
import { handleMenuJump } from '@/utils/navigation'
import { useMenuBadge } from '@/composables/useMenuBadge'
// 类型定义
interface Props {
@@ -66,6 +70,9 @@
(e: 'close'): void
}>()
// 使用菜单徽标系统
const { isMenuBadgeVisible, getMenuTextBadge } = useMenuBadge()
// 计算属性
const filteredMenuItems = computed(() => filterRoutes(props.list))
+264
View File
@@ -0,0 +1,264 @@
import { computed } from 'vue'
import { useMenuBadgeStore } from '@/store/modules/menuBadge'
import { useMenuStore } from '@/store/modules/menu'
import mittBus from '@/utils/sys/mittBus'
// 全局初始化状态
let globalInitialized = false
// 重置全局初始化状态(用于退出登录后重新初始化)
export const resetGlobalInitialized = () => {
globalInitialized = false
// console.log('徽标系统全局初始化状态已重置')
}
/**
* 菜单徽标管理 Composable
* 提供菜单徽标的统一管理接口
*/
export function useMenuBadge() {
const menuBadgeStore = useMenuBadgeStore()
const menuStore = useMenuStore()
// 计算属性:获取菜单列表(带徽标状态)
const menuListWithBadges = computed(() => {
const menuList = menuStore.menuList
if (!menuList.length) return menuList
// 同步徽标状态到菜单列表
menuBadgeStore.syncBadgeWithMenuList(menuList)
return menuList
})
// 更新菜单徽标
const updateMenuBadge = (path: string, count: number, type: 'dot' | 'text' = 'text') => {
menuBadgeStore.updateBadgeData(path, count, type)
}
// 批量更新菜单徽标
const batchUpdateMenuBadges = (
updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }>
) => {
menuBadgeStore.batchUpdateBadgeData(updates)
}
// 清除菜单徽标
const clearMenuBadge = (path: string) => {
menuBadgeStore.clearBadge(path)
}
// 清除所有菜单徽标
const clearAllMenuBadges = () => {
menuBadgeStore.clearAllBadges()
}
// 获取菜单徽标数量
const getMenuBadgeCount = (path: string) => {
return menuBadgeStore.getBadgeCount(path)
}
// 检查菜单是否显示徽标
const isMenuBadgeVisible = (path: string) => {
return menuBadgeStore.shouldShowBadge(path)
}
// 获取菜单文本徽标
const getMenuTextBadge = (path: string) => {
return menuBadgeStore.shouldShowTextBadge(path)
}
// 处理WebSocket徽标更新消息
const handleWebSocketBadgeUpdate = (data: any) => {
// console.log('收到WebSocket徽标更新消息:', data)
try {
// 根据消息类型处理不同的徽标更新
if (data.event === 'menu.badge.update') {
const { path, count, type } = data.data
if (path && typeof count === 'number') {
updateMenuBadge(path, count, type || 'text')
}
} else if (data.event === 'menu.badge.batch_update') {
const { updates } = data.data
if (Array.isArray(updates)) {
batchUpdateMenuBadges(updates)
}
} else if (data.event === 'menu.badge.clear') {
const { path } = data.data
if (path) {
clearMenuBadge(path)
} else {
clearAllMenuBadges()
}
}
} catch (error) {
console.error('处理WebSocket徽标更新消息时出错:', error, data)
}
}
// 处理菜单徽标数据数组格式
const handleMenuBadgeDataArray = (data: any) => {
// console.log('收到菜单徽标数据数组:', data)
if (Array.isArray(data.data)) {
const updates = data.data
.filter((item: any) => item.url && typeof item.badge === 'number')
.map((item: any) => ({
path: item.url,
count: item.badge,
type: 'text' as const
}))
if (updates.length > 0) {
// console.log('批量更新菜单徽标:', updates)
batchUpdateMenuBadges(updates)
}
}
}
// 处理通知相关的徽标更新
const handleNotificationBadgeUpdate = (data: any) => {
// console.log('收到通知徽标更新消息:', data)
// 根据通知类型更新对应菜单的徽标
if (data.event === 'notify.receive') {
// 通知接收,更新通知菜单徽标
const currentCount = getMenuBadgeCount('/notification')
updateMenuBadge('/notification', currentCount + 1, 'text')
} else if (data.event === 'notify.read') {
// 通知已读,减少通知菜单徽标
const currentCount = getMenuBadgeCount('/notification')
updateMenuBadge('/notification', Math.max(0, currentCount - 1), 'text')
} else if (data.event === 'notify.clear') {
// 清除所有通知,清除通知菜单徽标
clearMenuBadge('/notification')
}
}
// 处理审批相关的徽标更新
const handleApprovalBadgeUpdate = (data: any) => {
// console.log('收到审批徽标更新消息:', data)
if (data.event === 'approval.new') {
// 新审批,更新审批菜单徽标
const currentCount = getMenuBadgeCount('/admin/approval')
updateMenuBadge('/admin/approval', currentCount + 1, 'text')
} else if (data.event === 'approval.processed') {
// 审批已处理,减少审批菜单徽标
const currentCount = getMenuBadgeCount('/admin/approval')
updateMenuBadge('/admin/approval', Math.max(0, currentCount - 1), 'text')
}
}
// 处理库存相关的徽标更新
const handleStockBadgeUpdate = (data: any) => {
// console.log('收到库存徽标更新消息:', data)
if (data.event === 'stock.low') {
// 库存不足,更新库存菜单徽标
const currentCount = getMenuBadgeCount('/admin/stock')
updateMenuBadge('/admin/stock', currentCount + 1, 'dot')
} else if (data.event === 'stock.normal') {
// 库存正常,减少库存菜单徽标
const currentCount = getMenuBadgeCount('/admin/stock')
updateMenuBadge('/admin/stock', Math.max(0, currentCount - 1), 'dot')
}
}
// 统一的WebSocket消息处理
const handleWebSocketMessage = (data: any) => {
if (!data || !data.event) return
// 处理菜单徽标数据数组格式(如 approval.todo.counts)
if (data.event.includes('.todo.counts') || data.event.includes('.badge.counts')) {
handleMenuBadgeDataArray(data)
} else if (data.event.includes('menu.badge')) {
handleWebSocketBadgeUpdate(data)
} else if (data.event.includes('notify')) {
handleNotificationBadgeUpdate(data)
} else if (data.event.includes('approval')) {
handleApprovalBadgeUpdate(data)
} else if (data.event.includes('stock')) {
handleStockBadgeUpdate(data)
}
}
// 初始化徽标系统
const initBadgeSystem = () => {
if (globalInitialized) {
// console.log('菜单徽标系统已经全局初始化,跳过重复初始化')
return
}
// console.log('初始化菜单徽标系统')
globalInitialized = true
// 标记页面已加载完成
menuBadgeStore.setPageReady(true)
// 先清理可能存在的旧监听器,避免重复注册
mittBus.off('ws:message', handleWebSocketMessage)
mittBus.off('menu-badge-update', handleWebSocketBadgeUpdate)
mittBus.off('approval-badge-update', handleApprovalBadgeUpdate)
mittBus.off('stock-badge-update', handleStockBadgeUpdate)
mittBus.off('notification-badge-update', handleNotificationBadgeUpdate)
mittBus.off('menu:updated')
// 监听WebSocket消息
mittBus.on('ws:message', handleWebSocketMessage)
// 监听特定的徽标更新事件
mittBus.on('menu-badge-update', handleWebSocketBadgeUpdate)
mittBus.on('approval-badge-update', handleApprovalBadgeUpdate)
mittBus.on('stock-badge-update', handleStockBadgeUpdate)
mittBus.on('notification-badge-update', handleNotificationBadgeUpdate)
// 监听菜单数据变化,同步徽标状态
mittBus.on('menu:updated', () => {
menuBadgeStore.syncBadgeWithMenuList(menuStore.menuList)
})
}
// 清理徽标系统
const cleanupBadgeSystem = () => {
if (!globalInitialized) {
// console.log('菜单徽标系统未全局初始化,跳过清理')
return
}
// console.log('清理菜单徽标系统')
globalInitialized = false
// 移除事件监听
mittBus.off('ws:message', handleWebSocketMessage)
mittBus.off('menu-badge-update', handleWebSocketBadgeUpdate)
mittBus.off('approval-badge-update', handleApprovalBadgeUpdate)
mittBus.off('stock-badge-update', handleStockBadgeUpdate)
mittBus.off('notification-badge-update', handleNotificationBadgeUpdate)
mittBus.off('menu:updated')
// 重置页面加载状态
menuBadgeStore.setPageReady(false)
}
// 注意:不在组件中自动初始化,避免重复初始化
// 初始化应该在App.vue中全局调用一次
return {
// 计算属性
menuListWithBadges,
// 方法
updateMenuBadge,
batchUpdateMenuBadges,
clearMenuBadge,
clearAllMenuBadges,
getMenuBadgeCount,
isMenuBadgeVisible,
getMenuTextBadge,
// 系统方法
initBadgeSystem,
cleanupBadgeSystem,
handleWebSocketMessage
}
}
+1 -1
View File
@@ -407,7 +407,7 @@ export function useStockingForm(isFinal = false, stockingId?: string | number) {
// 金额包装方法
const wrapMoney = (amount: any, currency: string, remark: string) => ({
currency: currency || '',
amount: amount === undefined || amount === null ? '' : String(amount),
amount: amount === undefined || amount === null ? 0 : String(amount),
rate: getCurrencyRate(currency),
remark
})
+303
View File
@@ -0,0 +1,303 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { AppRouteRecord } from '@/types/router'
import { useUserStore } from './user'
// 菜单徽标数据类型
export interface MenuBadgeData {
/** 菜单路径 */
path: string
/** 徽标数量 */
count: number
/** 徽标类型:dot(红点) | text(数字) */
type: 'dot' | 'text'
/** 最后更新时间 */
lastUpdated: number
}
// 菜单徽标Store
export const useMenuBadgeStore = defineStore(
'menuBadgeStore',
() => {
// 徽标数据映射 - 使用普通对象而不是Map,避免持久化问题
const badgeData = ref<Record<string, MenuBadgeData>>({})
// 页面是否已加载完成
const isPageReady = ref(false)
// 待处理的徽标更新队列(页面未加载完成时使用)
const pendingUpdates = ref<Array<{ path: string; count: number; type: 'dot' | 'text' }>>([])
// 获取当前用户ID
const getCurrentUserId = (): string => {
try {
const userStore = useUserStore()
return userStore.info?.userId?.toString() || 'anonymous'
} catch {
return 'anonymous'
}
}
// 获取用户特定的存储key
const getUserStorageKey = (): string => {
const userId = getCurrentUserId()
return `menu-badge-${userId}`
}
// 从localStorage加载用户特定的徽标数据
const loadUserBadgeData = () => {
try {
const storageKey = getUserStorageKey()
const stored = localStorage.getItem(storageKey)
if (stored) {
const parsed = JSON.parse(stored)
badgeData.value = parsed.badgeData || {}
isPageReady.value = parsed.isPageReady || false
pendingUpdates.value = parsed.pendingUpdates || []
}
} catch (error) {
console.warn('加载用户徽标数据失败:', error)
}
}
// 保存用户特定的徽标数据到localStorage
const saveUserBadgeData = () => {
try {
const storageKey = getUserStorageKey()
const data = {
badgeData: badgeData.value,
isPageReady: isPageReady.value,
pendingUpdates: pendingUpdates.value
}
localStorage.setItem(storageKey, JSON.stringify(data))
} catch (error) {
console.warn('保存用户徽标数据失败:', error)
}
}
// 清理其他用户的徽标数据
const cleanupOtherUserData = () => {
try {
const currentUserId = getCurrentUserId()
const keys = Object.keys(localStorage)
keys.forEach((key) => {
if (key.startsWith('menu-badge-') && !key.endsWith(`-${currentUserId}`)) {
localStorage.removeItem(key)
}
})
} catch (error) {
console.warn('清理其他用户数据失败:', error)
}
}
// 计算属性:获取所有徽标数据
const allBadgeData = computed(() => {
return { ...badgeData.value }
})
// 获取指定菜单的徽标数据
const getBadgeData = (path: string): MenuBadgeData | null => {
return badgeData.value[path] || null
}
// 获取指定菜单的徽标数量
const getBadgeCount = (path: string): number => {
const data = badgeData.value[path]
return data?.count || 0
}
// 检查菜单是否应该显示徽标
const shouldShowBadge = (path: string): boolean => {
const data = badgeData.value[path]
return Boolean(data && data.count > 0)
}
// 检查菜单是否应该显示文本徽标
const shouldShowTextBadge = (path: string): string | null => {
const data = badgeData.value[path]
if (data && data.count > 0 && data.type === 'text') {
return data.count > 99 ? '99+' : data.count.toString()
}
return null
}
// 设置页面加载状态
const setPageReady = (ready: boolean) => {
isPageReady.value = ready
// 页面加载完成时,加载用户特定的徽标数据
if (ready) {
loadUserBadgeData()
// 处理待处理的更新
if (pendingUpdates.value.length > 0) {
// console.log('页面加载完成,处理待处理的徽标更新:', pendingUpdates.value.length)
pendingUpdates.value.forEach((update) => {
updateBadgeData(update.path, update.count, update.type)
})
pendingUpdates.value = []
}
}
}
// 更新徽标数据
const updateBadgeData = (path: string, count: number, type: 'dot' | 'text' = 'text') => {
// 如果页面未加载完成,将更新加入队列
if (!isPageReady.value) {
// console.log('页面未加载完成,将徽标更新加入队列:', { path, count, type })
// 检查是否已存在相同路径的待处理更新,避免重复
const existingIndex = pendingUpdates.value.findIndex((item) => item.path === path)
if (existingIndex >= 0) {
pendingUpdates.value[existingIndex] = { path, count, type }
} else {
pendingUpdates.value.push({ path, count, type })
}
// 保存到用户特定存储
saveUserBadgeData()
return
}
// console.log('更新菜单徽标:', { path, count, type })
if (count <= 0) {
// 如果数量为0或负数,移除徽标
delete badgeData.value[path]
} else {
// 更新或创建徽标数据
badgeData.value[path] = {
path,
count,
type,
lastUpdated: Date.now()
}
}
// 保存到用户特定存储
saveUserBadgeData()
}
// 批量更新徽标数据
const batchUpdateBadgeData = (
updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }>
) => {
if (!isPageReady.value) {
// console.log('页面未加载完成,将批量徽标更新加入队列:', updates.length)
updates.forEach((update) => {
const existingIndex = pendingUpdates.value.findIndex((item) => item.path === update.path)
if (existingIndex >= 0) {
pendingUpdates.value[existingIndex] = {
path: update.path,
count: update.count,
type: update.type || 'text'
}
} else {
pendingUpdates.value.push({
path: update.path,
count: update.count,
type: update.type || 'text'
})
}
})
// 保存到用户特定存储
saveUserBadgeData()
return
}
// console.log('批量更新菜单徽标:', updates.length)
updates.forEach((update) => {
updateBadgeData(update.path, update.count, update.type || 'text')
})
// 批量更新后保存
saveUserBadgeData()
}
// 清除指定菜单的徽标
const clearBadge = (path: string) => {
updateBadgeData(path, 0)
}
// 清除所有徽标
const clearAllBadges = () => {
badgeData.value = {}
pendingUpdates.value = []
// 保存到用户特定存储
saveUserBadgeData()
}
// 从菜单列表同步徽标状态
const syncBadgeWithMenuList = (menuList: AppRouteRecord[]) => {
if (!isPageReady.value) {
// console.log('页面未加载完成,跳过菜单徽标同步')
return
}
const updateMenuBadge = (menus: AppRouteRecord[]) => {
menus.forEach((menu) => {
const badgeData = getBadgeData(menu.path)
if (badgeData) {
// 更新菜单的徽标状态
menu.meta.showBadge = badgeData.type === 'dot' && badgeData.count > 0
menu.meta.showTextBadge =
badgeData.type === 'text' && badgeData.count > 0
? badgeData.count > 99
? '99+'
: badgeData.count.toString()
: undefined
} else {
// 清除菜单的徽标状态
menu.meta.showBadge = false
menu.meta.showTextBadge = undefined
}
// 递归处理子菜单
if (menu.children) {
updateMenuBadge(menu.children)
}
})
}
updateMenuBadge(menuList)
}
// 获取待处理更新的数量
const getPendingUpdatesCount = () => pendingUpdates.value.length
// 获取待处理的更新列表
const getPendingUpdates = () => [...pendingUpdates.value]
return {
// 状态
badgeData,
isPageReady,
pendingUpdates,
// 计算属性
allBadgeData,
// 方法
getBadgeData,
getBadgeCount,
shouldShowBadge,
shouldShowTextBadge,
setPageReady,
updateBadgeData,
batchUpdateBadgeData,
clearBadge,
clearAllBadges,
syncBadgeWithMenuList,
getPendingUpdatesCount,
getPendingUpdates,
// 用户隔离相关方法
loadUserBadgeData,
saveUserBadgeData,
cleanupOtherUserData,
getCurrentUserId
}
},
{
persist: {
key: 'menu-badge',
storage: localStorage
}
}
)
+14
View File
@@ -140,6 +140,20 @@ export const useUserStore = defineStore(
}
const logOut = () => {
// 清理徽标系统
import('@/composables/useMenuBadge').then(({ useMenuBadge, resetGlobalInitialized }) => {
const { cleanupBadgeSystem, clearAllMenuBadges } = useMenuBadge()
clearAllMenuBadges()
cleanupBadgeSystem()
resetGlobalInitialized()
})
// 清理徽标store中的其他用户数据
import('@/store/modules/menuBadge').then(({ useMenuBadgeStore }) => {
const menuBadgeStore = useMenuBadgeStore()
menuBadgeStore.cleanupOtherUserData()
})
// 清空所有本地缓存
localStorage.clear()
sessionStorage.clear()
+186
View File
@@ -0,0 +1,186 @@
/**
* 菜单徽标管理工具
* 用于管理菜单徽标功能
*/
import { useMenuBadgeStore } from '@/store/modules/menuBadge'
/**
* 菜单徽标管理类
*/
export class MenuBadgeTest {
private menuBadgeStore = useMenuBadgeStore()
/**
* 直接更新徽标
*/
updateBadge(path: string, count: number, type: 'dot' | 'text' = 'text') {
// console.log(`直接更新徽标: ${path} = ${count} (${type})`)
this.menuBadgeStore.updateBadgeData(path, count, type)
}
/**
* 批量更新徽标
*/
batchUpdateBadges(updates: Array<{ path: string; count: number; type?: 'dot' | 'text' }>) {
// console.log('批量更新徽标:', updates)
this.menuBadgeStore.batchUpdateBadgeData(updates)
}
/**
* 清除徽标
*/
clearBadge(path: string) {
// console.log(`清除徽标: ${path}`)
this.menuBadgeStore.clearBadge(path)
}
/**
* 清除所有徽标
*/
clearAllBadges() {
// console.log('清除所有徽标')
this.menuBadgeStore.clearAllBadges()
}
/**
* 获取徽标状态
*/
getBadgeStatus(path: string) {
const data = this.menuBadgeStore.getBadgeData(path)
// console.log(`徽标状态 ${path}:`, data)
return data
}
/**
* 获取所有徽标状态
*/
getAllBadgeStatus() {
const allData = this.menuBadgeStore.allBadgeData
// console.log('所有徽标状态:', allData)
return allData
}
/**
* 获取待处理更新数量
*/
getPendingUpdatesCount() {
const count = this.menuBadgeStore.getPendingUpdatesCount()
// console.log(`待处理更新数量: ${count}`)
return count
}
/**
* 设置页面加载状态
*/
setPageReady(ready: boolean) {
// console.log(`设置页面加载状态: ${ready}`)
this.menuBadgeStore.setPageReady(ready)
}
/**
* 检查实际菜单路径
*/
checkActualMenuPaths() {
// console.log('检查实际菜单路径...')
try {
// 获取菜单store
import('@/store/modules/menu')
.then(({ useMenuStore }) => {
const menuStore = useMenuStore()
this.printMenuPaths(menuStore.menuList)
})
.catch((error) => {
console.error('动态导入菜单store失败:', error)
})
} catch (error) {
console.error('检查菜单路径失败:', error)
}
}
private printMenuPaths(menuList: any[]) {
// console.log('当前菜单列表:', menuList)
// 递归打印所有菜单路径
const printMenuPaths = (menus: any[], level = 0) => {
menus.forEach((menu) => {
// const indent = ' '.repeat(level)
// console.log(`${indent}${menu.path} - ${menu.meta?.title}`)
if (menu.children && menu.children.length > 0) {
printMenuPaths(menu.children, level + 1)
}
})
}
if (menuList && menuList.length > 0) {
printMenuPaths(menuList)
} else {
console.log('菜单列表为空,可能还未加载完成')
}
}
/**
* 运行基础功能测试
*/
runBasicTest() {
// console.log('开始运行基础功能测试...')
// 1. 测试页面未加载完成时的更新
// console.log('\n=== 测试页面未加载完成时的更新 ===')
this.setPageReady(false)
this.updateBadge('/admin/approval', 3, 'text')
this.updateBadge('/admin/stock', 1, 'dot')
// 2. 设置页面加载完成
// console.log('\n=== 设置页面加载完成 ===')
this.setPageReady(true)
// 3. 测试批量更新
// console.log('\n=== 测试批量更新 ===')
this.batchUpdateBadges([
{ path: '/admin/approval', count: 3, type: 'text' },
{ path: '/admin/stock', count: 1, type: 'dot' }
])
// 4. 显示最终状态
// console.log('\n=== 最终状态 ===')
this.getAllBadgeStatus()
// console.log('\n基础功能测试完成!')
}
/**
* 检查WebSocket连接状态
*/
checkWebSocketStatus() {
// console.log('检查WebSocket连接状态...')
// 检查全局WebSocket状态函数
if (typeof window !== 'undefined' && (window as any).getWebSocketStatus) {
const status = (window as any).getWebSocketStatus()
console.log('WebSocket状态:', status)
} else {
console.log('WebSocket状态函数不可用')
}
// 检查菜单徽标系统初始化状态
const pendingCount = this.menuBadgeStore.getPendingUpdatesCount()
console.log('待处理的徽标更新数量:', pendingCount)
if (pendingCount > 0) {
console.log('待处理的更新:', this.menuBadgeStore.getPendingUpdates())
}
}
}
// 创建全局测试实例
const menuBadgeTest = new MenuBadgeTest()
// 将测试工具挂载到全局对象,方便在控制台使用
if (typeof window !== 'undefined') {
// @ts-expect-error - 挂载到全局对象用于调试
window.menuBadgeTest = menuBadgeTest
}
export default menuBadgeTest
+94 -8
View File
@@ -1,4 +1,4 @@
import mittBus from './mittBus'
import mittBus from './mittBus'
import { useUserStore } from '@/store/modules/user'
import { ElNotification } from 'element-plus'
@@ -94,10 +94,14 @@ const handleMessage = (event: MessageEvent) => {
let data: any = event.data
try {
data = JSON.parse(event.data)
} catch {
// 忽略解析错误
} catch (error) {
// console.warn('WebSocket消息解析失败:', error, '原始数据:', event.data)
// 对于无法解析的消息,仍然尝试处理原始数据
data = { event: 'unknown', data: event.data }
}
console.log('WebSocket message:', data)
// 处理通知消息
if (data.event == 'notify.receive') {
ElNotification({
title: data.data.notice_data.message.title || '通知',
@@ -106,22 +110,85 @@ const handleMessage = (event: MessageEvent) => {
})
mittBus.emit('notification-data-changed', 1)
}
// 处理菜单徽标更新消息
if (data.event && data.event.includes('menu.badge')) {
// console.log('收到菜单徽标更新消息:', data)
// 直接触发徽标更新事件
mittBus.emit('menu-badge-update', data)
}
// 处理审批相关消息
if (data.event && data.event.includes('approval')) {
// console.log('收到审批相关消息:', data)
// 如果是审批待办数量消息,直接触发徽标更新
if (data.event.includes('.todo.counts') || data.event.includes('.badge.counts')) {
mittBus.emit('menu-badge-update', data)
} else {
// 其他审批消息触发审批徽标更新事件
mittBus.emit('approval-badge-update', data)
}
}
// 处理库存相关消息
if (data.event && data.event.includes('stock')) {
// console.log('收到库存相关消息:', data)
// 触发库存徽标更新事件
mittBus.emit('stock-badge-update', data)
}
// 处理通知相关消息
if (data.event && data.event.includes('notify')) {
// console.log('收到通知相关消息:', data)
// 触发通知徽标更新事件
mittBus.emit('notification-badge-update', data)
}
// 处理其他可能的徽标相关消息
if (data.event && (data.event.includes('badge') || data.event.includes('count'))) {
// console.log('收到其他徽标相关消息:', data)
// 尝试作为通用徽标更新处理
mittBus.emit('menu-badge-update', data)
}
// 发送原始消息到事件总线
mittBus.emit('ws:message', data)
// 检查WebSocket连接状态,如果连接异常则记录
if (socket && socket.readyState !== WebSocket.OPEN) {
// console.warn('WebSocket连接状态异常,当前状态:', socket.readyState, '消息可能丢失')
}
}
// 设置WebSocket事件处理器
const setupWebSocketHandlers = (ws: WebSocket) => {
ws.onopen = () => {
console.log('WebSocket连接已建立')
isInitialConnection = false
isConnecting = false
startHeartbeat()
mittBus.emit('ws:open')
sendPing()
// 连接建立成功后推送一个连接确认消息
const connectMessage = {
event: 'system.connect',
data: {
timestamp: Date.now(),
message: 'WebSocket连接已建立'
},
trace_id: generateTraceId()
}
try {
ws.send(JSON.stringify(connectMessage))
} catch (error) {
console.error('发送连接确认消息失败:', error)
}
}
ws.onmessage = handleMessage
ws.onerror = (err) => {
console.error('WebSocket连接错误:', err)
mittBus.emit('ws:error', err)
isConnecting = false
try {
@@ -134,6 +201,7 @@ const setupWebSocketHandlers = (ws: WebSocket) => {
}
ws.onclose = () => {
console.log('WebSocket连接已关闭')
mittBus.emit('ws:close')
isConnecting = false
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
@@ -187,32 +255,50 @@ const scheduleReconnect = () => {
// 初始化WebSocket连接
export const initWebSocket = (token: string) => {
try {
if (!token) return
if (!token) {
console.log('WebSocket初始化失败:没有token')
return
}
const now = Date.now()
// 简单防抖:在短时间内重复触发则跳过
if (now - lastInitAt < INIT_DEBOUNCE_MS) {
console.log('WebSocket初始化被防抖跳过')
return
}
// 避免并发初始化:已在连接中或已连接且 token 未变化则跳过
if (isConnecting) return
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
if (isConnecting) {
console.log('WebSocket正在连接中,跳过重复初始化')
return
}
if (
socket &&
(socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)
) {
if (lastInitToken === token) {
console.log('WebSocket已连接且token未变化,跳过初始化')
return
}
// token 变更:需要切换连接
try { socket.close() } catch {}
console.log('WebSocket token变更,关闭旧连接')
try {
socket.close()
} catch {
// 忽略关闭错误
}
socket = null
}
cleanup()
const url = `${WS_BASE}${token}`
console.log('正在初始化WebSocket连接:', url)
isConnecting = true
lastInitToken = token
lastInitAt = now
socket = new WebSocket(url)
setupWebSocketHandlers(socket)
} catch (e) {
console.error('WebSocket初始化失败:', e)
mittBus.emit('ws:error', e)
isConnecting = false
scheduleReconnect()
@@ -270,4 +356,4 @@ export const getWebSocketStatus = () => ({
isInitialConnection,
isConnecting,
lastInitToken
})
})
+22 -1
View File
@@ -425,7 +425,13 @@
// const businessUsers = ref([])
// 选项缓存,避免重复请求
const optionsCache = reactive({ role: [], user: [], department: [] })
const optionsCache = reactive({
role: [],
user: [],
department: [],
grouplist: [],
group_members: []
})
// 懒加载并缓存不同类型的选项
const ensureOptionsLoaded = async (group) => {
@@ -449,6 +455,21 @@
optionsCache.user = response || []
}
group.options = optionsCache.user
} else if (type === 'scenario_leader_of_initiator') {
if (!optionsCache.grouplist || optionsCache.grouplist.length === 0) {
const response = await commonService.getUserGroupScene()
optionsCache.grouplist = response || []
}
group.options = optionsCache.grouplist
} else if (type === 'group_members') {
if (!optionsCache.group_members || optionsCache.group_members.length === 0) {
const response = await commonService.getUserGroupSelect()
response.forEach((group) => {
group.name = group.title
})
optionsCache.group_members = response || []
}
group.options = optionsCache.group_members
} else {
group.options = []
}
+29 -3
View File
@@ -453,6 +453,21 @@
optionsCache.user = response || []
}
group.options = optionsCache.user
} else if (type === 'scenario_leader_of_initiator') {
if (!optionsCache.grouplist || optionsCache.grouplist.length === 0) {
const response = await commonService.getUserGroupScene()
optionsCache.grouplist = response || []
}
group.options = optionsCache.grouplist
} else if (type === 'group_members') {
if (!optionsCache.group_members || optionsCache.group_members.length === 0) {
const response = await commonService.getUserGroupSelect()
response.forEach((group) => {
group.name = group.title
})
optionsCache.group_members = response || []
}
group.options = optionsCache.group_members
} else {
group.options = []
}
@@ -590,10 +605,21 @@
if (ap.type === 'initiator_leader') {
group.users = []
} else if (ap.value) {
group.users = String(ap.value)
const values = String(ap.value)
.split(',')
.map((v) => Number((v || '').trim()))
.filter((n) => !Number.isNaN(n))
.map((v) => (v || '').trim())
.filter(Boolean)
// 判断是否为纯数字ID
const isNumericIds = values.every(
(v) => !Number.isNaN(Number(v)) && Number.isInteger(Number(v))
)
if (isNumericIds) {
// 如果是数字ID,转换为数字数组
group.users = values.map((v) => Number(v))
} else {
// 如果是字符串名称,保持为字符串数组
group.users = values
}
}
node.approverGroups.push(group)
}
+68 -22
View File
@@ -29,7 +29,13 @@
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140" />
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140">
<template #default="scope">
<span :style="{ color: getFlowTypeColor(scope.row.flow_type) }">
{{ scope.row.flows_name }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120">
<template #default="scope">
@@ -79,7 +85,9 @@
>
<div class="approval-content">
<!-- 头部信息 -->
<div v-if="orderDialog.type == 'stock_orders'">
<div
v-if="orderDialog.type == 'stock_orders' || orderDialog.type == 'stock_orders_nullify'"
>
<OrderDetailContent :orderData="orderDialog.orderData" />
</div>
<div v-if="orderDialog.type == 'purchase_orders'">
@@ -114,7 +122,12 @@
<ElButton type="primary" size="large" class="submit-btn" @click="handleApprove('approve')"
>同意</ElButton
>
<ElButton type="warning" size="large" class="submit-btn" @click="handleTransfer()"
<ElButton
v-if="orderDialog.type == 'purchase_orders'"
type="warning"
size="large"
class="submit-btn"
@click="handleTransfer()"
>转交</ElButton
>
</div>
@@ -178,6 +191,26 @@
userOptions.value = res || []
// return res || []
}
// 根据 flow_type 获取审批单名称的字体颜色
const getFlowTypeColor = (flowType: string): string => {
switch (flowType) {
case 'stock_orders':
return '#409EFF' // 蓝色 - 备货单
case 'stock_orders_nullify':
return '#F56C6C' // 红色 - 备货单作废
case 'purchase_orders':
return '#67C23A' // 绿色 - 采购单
case 'purchase_orders_nullify':
return '#E6A23C' // 橙色 - 采购单作废
case 'return_orders':
return '#909399' // 灰色 - 退货单
case 'refund_orders':
return '#9C27B0' // 紫色 - 退款单
default:
return '#303133' // 默认黑色
}
}
const zjForm = reactive({
to_uid: '',
instance_id: '',
@@ -258,25 +291,38 @@
id: row.id,
flow_type: row.flow_type
})
if (row.flow_type == 'stock_orders') {
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
} else if (row.flow_type == 'purchase_orders') {
const orderData: OrderData = res
const urls = res.contract_file_url
const paths = res.contract_file
const list: Array<{ name: string; url: string }> = []
const nameFromPath = (p: string) => extractFileName(p)
urls.forEach((u: string, i: number) => {
const p = paths[i] || ''
list.push({ name: p ? nameFromPath(p) : extractFileName(u), url: u })
})
orderData.contract_file_url = list
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-采购单(采购)'
console.log(res)
switch (row.flow_type) {
case 'stock_orders': {
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
break
}
case 'stock_orders_nullify': {
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '备货单-作废'
break
}
case 'purchase_orders': {
const orderData: OrderData = res
const urls = res.contract_file_url
const paths = res.contract_file
const list: Array<{ name: string; url: string }> = []
const nameFromPath = (p: string) => extractFileName(p)
urls.forEach((u: string, i: number) => {
const p = paths[i] || ''
list.push({ name: p ? nameFromPath(p) : extractFileName(u), url: u })
})
orderData.contract_file_url = list
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-采购单(采购)'
break
}
}
approvalDialogVisible.value = true
}
@@ -98,13 +98,23 @@
<div class="form-card" v-if="status == 'pending'">
<!-- 底部信息 -->
<div class="submit-wrap">
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleSubmit('reject')"
<ElButton
type="primary"
plain
size="large"
class="submit-btn"
@click="handleSubmit('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleSubmit('approve')"
>同意</ElButton
>
<ElButton type="warning" size="large" class="submit-btn" @click="handleTransfer()"
<ElButton
v-if="flow_type == 'purchase_orders'"
type="warning"
size="large"
class="submit-btn"
@click="handleTransfer()"
>转交</ElButton
>
</div>
@@ -32,7 +32,13 @@
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140" />
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140">
<template #default="scope">
<span :style="{ color: getFlowTypeColor(scope.row.flow_type) }">
{{ scope.row.flows_name }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
<ElTableColumn label="审批结果" prop="action_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120">
@@ -158,7 +164,25 @@
onMounted(() => {
getList()
})
// 根据 flow_type 获取审批单名称的字体颜色
const getFlowTypeColor = (flowType: string): string => {
switch (flowType) {
case 'stock_orders':
return '#409EFF' // 蓝色 - 备货单
case 'stock_orders_nullify':
return '#F56C6C' // 红色 - 备货单作废
case 'purchase_orders':
return '#67C23A' // 绿色 - 采购单
case 'purchase_orders_nullify':
return '#E6A23C' // 橙色 - 采购单作废
case 'return_orders':
return '#909399' // 灰色 - 退货单
case 'refund_orders':
return '#9C27B0' // 紫色 - 退款单
default:
return '#303133' // 默认黑色
}
}
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
@@ -221,7 +221,7 @@
getList()
})
function statusClass(status: any) {
if (status === 'rejected') return 'status-lost'
if (status === 'rejected' || status === 'voided') return 'status-lost'
if (status === 'approved') return 'status-following'
return 'status-dealed'
}
+281
View File
@@ -0,0 +1,281 @@
<template>
<div class="page-content">
<div v-if="!isMobile">
<ElRow>
<div style="width: 12px"></div>
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('group@add')">新增</ElButton>
</ElCol>
</ElRow>
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="分组名称" prop="title" width="160px" />
<ElTableColumn label="负责人" prop="id" width="120px">
<template #default="scope">
<ElTag>{{ scope.row.leader_name }}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="分组成员" prop="id">
<template #default="scope">
<span v-for="item in scope.row.user" :key="item.id" style="margin-right: 10px;"> {{ item.name }}</span>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" width="160px" />
<ElTableColumn fixed="right" label="操作" width="110px">
<template #default="scope">
<ElRow>
<el-button
link
@click="showDialog('edit', scope.row)"
v-if="hasAuth('group@update')"
>
编辑
</el-button>
<el-button
type="danger"
link
@click="deleteRole(scope.row)"
v-if="hasAuth('group@destroy')"
>
删除
</el-button>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增' : '编辑'"
width="40%"
align-center
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="80px" class="form">
<ElFormItem label="分组名称" prop="title">
<ElInput v-model="form.title" />
</ElFormItem>
<ElFormItem label="分组成员" prop="user_ids">
<el-select
v-model="form.user_ids"
multiple
filterable
placeholder="请选择分组成员"
style="width: 100%"
>
<el-option
v-for="item in companyUserList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</ElFormItem>
<ElFormItem label="负责人" prop="leader_id">
<el-select
v-model="form.leader_id"
filterable
placeholder="请选择负责人"
style="width: 100%"
>
<el-option
v-for="item in companyUserList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</ElFormItem>
<ElFormItem label="业务类型" prop="scenario_code">
<ElInput v-model="form.scenario_code" />
</ElFormItem>
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" />
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
<div v-else class="mobile-hide-tip">
<p>该页面暂不支持移动端访问</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { userConService } from '@/api/admin/user'
import { useAuth } from '@/composables/useAuth'
import { manageService } from '@/api/adminApi'
const { hasAuth } = useAuth()
defineOptions({ name: 'Company' })
const loading = ref(false)
const dialogVisible = ref(false)
const formRef = ref<FormInstance>()
const { width } = useWindowSize()
const isMobile = computed(() => width.value < 500)
const rules = reactive<FormRules>({
title: [{ required: true, message: '请输入分组名称', trigger: 'blur' }],
user_ids: [{ required: true, message: '请选择分组成员', trigger: 'blur' }]
// tenant_key: [{ required: true, message: '请输入飞书标识', trigger: 'blur' }]
})
const companyUserList = ref<any>()
// 获取用户列表
const getCompanyUserList = async () => {
const response = await manageService.presetusers()
companyUserList.value = response || []
}
const form = reactive<any>({
title: '',
user_ids: [],
leader_id: '',
scenario_code: '',
sort: 0,
status: 1
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
// 表格数据
const tableData = ref<any[]>([])
// 表格实例引用
const tableRef = ref()
onMounted(() => {
getWorkList()
})
// 处理表格分页变化
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getWorkList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getWorkList()
}
// 获取列表数据
const getWorkList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const { records, current, size, total } = await userConService.getGroupList({
current: currentPage,
size: pageSize
})
tableData.value = records.map((item: any) => ({
...item
}))
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取列表失败:', error)
} finally {
loading.value = false
}
}
const dialogType = ref('add')
const showDialog = (type: string, row?: any) => {
getCompanyUserList()
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
form.title = row.title
form.id = row.id
form.leader_id = row.leader_id
form.scenario_code = row.scenario_code
form.user_ids = row.user.map((item: any) => item.id)
form.sort = row.sort
console.log(row.user_ids)
console.log(form.user_ids)
form.status = row.status
// form.tenant_key = row.tenant_key
} else {
form.title = ''
form.id = ''
form.scenario_code = ''
form.sort = 0
form.leader_id = ''
form.user_ids = []
form.status = 1
// form.tenant_key = ''
}
}
const deleteRole = (row?: any) => {
ElMessageBox.confirm('确定删除吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
const { message } = await userConService.deleteGroup({
id: row.id
})
ElMessage.success(message)
getWorkList()
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
if (dialogType.value === 'add') {
const { message } = await userConService.addGroup({
title: form.title,
user_ids: form.user_ids,
leader_id: form.leader_id,
sort: form.sort,
scenario_code: form.scenario_code,
status: form.status
})
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
} else {
const { message } = await userConService.editGroup(form)
ElMessage.success(message)
dialogVisible.value = false
getWorkList()
}
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.page-content {
.svg-icon {
width: 1.8em;
height: 1.8em;
overflow: hidden;
vertical-align: -8px;
fill: currentcolor;
}
}
</style>
File diff suppressed because it is too large Load Diff
+540 -66
View File
@@ -3,25 +3,46 @@
<!-- 搜索工具栏 -->
<div class="search-toolbar boxshadow">
<div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleAdd">新增车辆</ElButton>
</div>
<div class="toolbar-right">
<el-input
v-model="searchForm.keyword"
placeholder="请输入关键字"
clearable
style="width: 300px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
<el-form-item label="关键字:" label-width="70px">
<el-input
v-model="searchForm.keyword"
placeholder="请输入关键字"
clearable
@keyup.enter="getList"
>
</el-input>
</el-form-item>
<el-form-item label="品牌:" label-width="60px">
<ElSelect v-model="searchForm.brand_id" filterable placeholder="请选择品牌">
<ElOption label="全部" value="" />
<ElOption
v-for="item in brandOptions"
:key="item.brand_code || item.code"
:label="item.brand_name || item.name"
:value="item.id"
/>
</ElSelect>
</el-form-item>
<el-form-item label="车系:" label-width="60px">
<ElSelect v-model="searchForm.series_id" filterable placeholder="请选择车系">
<ElOption label="全部" value="" />
<ElOption
v-for="s in seriesOptions"
:key="s.series_code || s.code"
:label="s.series_name || s.name"
:value="s.id"
/>
</ElSelect>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList" v-ripple>搜索</el-button>
</el-form-item>
</div>
</div>
<!-- 数据表格 -->
<div class="table-container boxshadow">
<ElButton type="primary" v-ripple @click="handleAdd">新增车辆</ElButton>
<ArtTable
ref="tableRef"
row-key="id"
@@ -34,12 +55,12 @@
@current-change="handleCurrentChange"
>
<template #default>
<el-table-column prop="id" label="编码" min-width="170" show-overflow-tooltip align="center">
<!-- <el-table-column prop="id" label="编码" min-width="170" show-overflow-tooltip align="center">
<template #default="scope">
<div class="ellipsis" :title="scope.row.fingerprint">指纹:{{ scope.row.fingerprint }}</div>
<div class="ellipsis" :title="scope.row.sku_code">sku_code:{{ scope.row.sku_code }}</div>
</template>
</el-table-column>
</el-table-column> -->
<ElTableColumn label="车型" prop="model_name" min-width="150" align="center">
<template #default="scope">
<span class="primary">{{ scope.row.model_name }}</span>
@@ -48,7 +69,7 @@
<ElTableColumn label="品牌" prop="brand_name" min-width="100" align="center" />
<ElTableColumn label="车系" prop="series_name" min-width="120" align="center" />
<ElTableColumn label="年款" prop="model_year" align="center" />
<ElTableColumn label="版型" prop="trim" align="center" />
<!-- <ElTableColumn label="版型" prop="trim" align="center" />
<ElTableColumn label="配置等级" prop="trim_level" align="center" />
<ElTableColumn label="排量" prop="engine_disp_l" align="center">
<template #default="scope">
@@ -64,7 +85,7 @@
<template #default="scope">
{{ scope.row.range_km ? scope.row.range_km + 'km' : '-' }}
</template>
</ElTableColumn>
</ElTableColumn> -->
<ElTableColumn label="指导价(CNY)" prop="msrp" align="center" min-width="100">
<template #default="scope">
{{ scope.row.msrp ? scope.row.msrp : '-' }}
@@ -93,7 +114,6 @@
v-model="dialogVisible"
:title="dialogTitle"
width="1200px"
top="40px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
@@ -117,7 +137,7 @@
:value="item.brand_name || item.name"
/>
</ElSelect>
<ElInput v-model="formData.brand_code" placeholder="品牌编码" readonly disabled />
<!-- <ElInput v-model="formData.brand_code" placeholder="品牌编码" readonly disabled /> -->
</ElFormItem>
</ElCol>
<ElCol :span="8">
@@ -130,17 +150,74 @@
:value="s.series_name || s.name"
/>
</ElSelect>
<ElInput v-model="formData.series_code" placeholder="车系编码" readonly disabled />
<!-- <ElInput v-model="formData.series_code" placeholder="车系编码" readonly disabled /> -->
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="车型/配置名称" prop="model_name">
<div style="display: flex; align-items: center; gap: 8px">
<!-- 情况1: 有关联车型数据时显示下拉选择和采集按钮 -->
<template v-if="modelsOptions.length > 0">
<ElSelect
v-model="formData.model_name"
filterable
placeholder="请选择车型名称"
style="flex: 1"
:disabled="!formData.series_code"
allow-create
default-first-option
@change="handleModelSelectChange"
>
<ElOption
v-for="model in modelsOptions"
:key="model.model_code || model.code"
:label="model.model_name || model.name"
:value="model.model_name || model.name"
/>
</ElSelect>
<ElButton type="primary" size="small" @click="showCollectModelsDialog">
采集车型
</ElButton>
</template>
<!-- 情况2: 没有车型数据时显示采集按钮 -->
<template v-else-if="formData.series_code">
<ElInput
v-model="formData.model_name"
placeholder="请输入车型名称"
style="flex: 1"
@input="handleModelInputChange"
/>
<ElButton type="primary" size="small" @click="showCollectModelsDialog">
采集车型
</ElButton>
</template>
<!-- 情况3: 没有车系时直接输入 -->
<ElInput
v-else
v-model="formData.model_name"
placeholder="请输入车型名称"
style="flex: 1"
@input="handleModelInputChange"
/>
</div>
<!-- <ElInput
v-model="formData.model_code"
placeholder="车型编码"
readonly
disabled
style="margin-top: 8px"
/> -->
</ElFormItem>
</ElCol>
<!-- <ElCol :span="8">
<ElFormItem label="排量(L)" prop="engine_disp_l">
<ElInput v-model="formData.engine_disp_l" placeholder="请输入排量" />
<el-text class="mx-1" type="warning">如:1.5;纯电为空</el-text>
</ElFormItem>
</ElCol>
</ElCol> -->
</ElRow>
<ElRow :gutter="16">
<!-- <ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="版型/细分配置" prop="trim">
<ElSelect
@@ -208,14 +285,9 @@
<ElInput v-model="formData.edition_code" placeholder="版本编码" readonly disabled />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="车型/配置名称" prop="model_name">
<ElInput v-model="formData.model_name" placeholder="请输入车型名称" />
<el-text class="mx-1" type="warning">如:伊兰特,海狮07(不填则自动生成)</el-text>
</ElFormItem>
</ElCol>
</ElRow> -->
<!-- <ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="车型/配置代码" prop="model_code">
<ElInput v-model="formData.model_code" placeholder="请输入车型代码" />
@@ -228,7 +300,7 @@
<el-text class="mx-1" type="warning">如:EV多为1</el-text>
</ElFormItem>
</ElCol>
</ElRow>
</ElRow> -->
<ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="官方指导价" prop="msrp">
@@ -240,13 +312,13 @@
<ElInput v-model="formData.model_year" placeholder="YYYY" />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<!-- <ElCol :span="8">
<ElFormItem label="最大功率(kW)" prop="engine_power_kw">
<ElInput v-model="formData.engine_power_kw" placeholder="请输入最大功率" />
</ElFormItem>
</ElCol>
</ElCol> -->
</ElRow>
<ElRow :gutter="16">
<!-- <ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="电池容量(kWh)" prop="battery_kwh">
<ElInput v-model="formData.battery_kwh" placeholder="请输入电池容量" />
@@ -262,8 +334,8 @@
<ElInput v-model="formData.doors" placeholder="请输入门数" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="16">
</ElRow> -->
<!-- <ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="是否涡轮" prop="engine_turbo">
<ElSelect v-model="formData.engine_turbo" placeholder="请选择">
@@ -282,8 +354,8 @@
<ElInput v-model="formData.seats" placeholder="请输入座位数" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="16">
</ElRow> -->
<!-- <ElRow :gutter="16"> -->
<!-- <ElCol :span="8">
<ElFormItem label="原产国" prop="origin_country">
<ElSelect v-model="formData.origin_country" placeholder="请选择原产国" filterable>
@@ -304,7 +376,7 @@
</ElSelect>
</ElFormItem>
</ElCol> -->
<ElCol :span="8">
<!-- <ElCol :span="8">
<ElFormItem label="外观颜色" prop="color_exterior_text">
<ElSelect
v-model="formData.color_exterior_text"
@@ -327,8 +399,8 @@
disabled
/>
</ElFormItem>
</ElCol>
<ElCol :span="8">
</ElCol> -->
<!-- <ElCol :span="8">
<ElFormItem label="内饰颜色" prop="color_interior_text">
<ElSelect
v-model="formData.color_interior_text"
@@ -370,10 +442,10 @@
disabled
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElCol> -->
<!-- </ElRow> -->
<!-- <el-divider /> -->
<ElRow :gutter="16">
<!-- <ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="驱动类型" prop="drive_type">
<ElSelect v-model="formData.drive_type" placeholder="请选择驱动类型" clearable>
@@ -494,7 +566,7 @@
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElRow> -->
<ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="状态" prop="is_active">
@@ -852,18 +924,83 @@
</div>
</div>
</ElDialog>
<!-- 车型采集弹出层 -->
<ElDialog
v-model="collectModelsDialogVisible"
:title="`${currentCollectSeries?.series_name} - 车型采集`"
width="900px"
align-center
>
<div class="collect-models-container">
<div class="collect-models-header">
<div
style="
display: flex;
align-items: center;
color: var(--el-text-color-secondary);
font-size: 14px;
"
>
请选择一个车型
</div>
</div>
<div class="collect-models-content">
<div v-if="collectModelsLoading" class="loading-container">
<ElLoading text="正在采集车型数据..." />
</div>
<div v-else-if="collectModelsData.length === 0" class="empty-container">
<ElEmpty description="暂无车型数据" />
</div>
<div v-else class="models-table-container">
<ElTable :data="collectModelsData" style="width: 100%" max-height="400">
<ElTableColumn width="60" align="center">
<template #default="scope">
<ElRadio
:model-value="selectedModelId"
:value="scope.row.car_id || scope.row.title"
@change="selectModel(scope.row.car_id || scope.row.title)"
/>
</template>
</ElTableColumn>
<ElTableColumn prop="brand_name" label="品牌" width="100" />
<ElTableColumn prop="series_name" label="车系" width="120" />
<ElTableColumn prop="title" label="车型名称" min-width="200" show-overflow-tooltip />
<ElTableColumn prop="year" label="年款" width="80" />
<ElTableColumn prop="msrp" label="指导价" width="120">
<template #default="scope">
<span v-if="scope.row.msrp" class="price-text">
¥{{ formatPrice(scope.row.msrp) }}
</span>
<span v-else>-</span>
</template>
</ElTableColumn>
</ElTable>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="collectModelsDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="saveCollectModels" :disabled="!selectedModelId">
保存车型
</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { vehicleService } from '@/api/vehicleApi'
import { commonService } from '@/api/commonApi'
defineOptions({ name: 'AdminVehicleList' })
// 搜索表单
const searchForm = reactive({
keyword: ''
keyword: '',
brand_id: '',
series_id: ''
})
// 分页
@@ -947,6 +1084,16 @@
// 车系选项
const seriesOptions = ref<Array<any>>([])
// 车型选项
const modelsOptions = ref<Array<any>>([])
// 车型采集弹出层相关
const collectModelsDialogVisible = ref(false)
const currentCollectSeries = ref<any | null>(null)
const collectModelsLoading = ref(false)
const collectModelsData = ref<Array<any>>([])
const selectedModelId = ref<string>('')
// 外观颜色
const wcolorOptions = ref<Array<any>>([])
// 内饰颜色
@@ -992,6 +1139,30 @@
ElMessage.error('获取车系列表失败')
}
}
// 获取车型列表
const loadModelsOptions = async (seriesCode?: string) => {
if (!seriesCode) {
modelsOptions.value = []
return
}
try {
const matched = seriesOptions.value.find((s: any) => s.series_code === seriesCode)
const seriesId = matched ? matched.id : ''
if (!seriesId) {
modelsOptions.value = []
return
}
const response = await commonService.getModelsSelect({
series_id: seriesId
})
modelsOptions.value = response
} catch (error) {
console.error('获取车型列表失败:', error)
ElMessage.error('获取车型列表失败')
}
}
//颜色下拉列表
const loadColorOptions = async () => {
try {
@@ -1036,7 +1207,9 @@
const params = {
current: currentPage,
size: pageSize,
keyword: searchForm.keyword
keyword: searchForm.keyword,
brand_id: searchForm.brand_id,
series_id: searchForm.series_id
}
const { records, current, size, total } = await vehicleService.getVehicleList(params)
@@ -1062,22 +1235,122 @@
}
// 新增车辆
const handleAdd = () => {
loadBrandOptions()
loadColorOptions()
loadDictOptions()
const handleAdd = async () => {
dialogTitle.value = '新增车辆'
resetForm()
await loadBrandOptions()
loadColorOptions()
loadDictOptions()
dialogVisible.value = true
}
// 编辑车辆
const handleEdit = (row: any) => {
loadBrandOptions()
const handleEdit = async (row: any) => {
loadColorOptions()
loadDictOptions()
dialogTitle.value = '编辑车辆'
// 先清空表单和选项数据,避免监听器干扰
resetForm()
// 使用 nextTick 确保 DOM 更新后再设置数据
await nextTick()
// 设置表单数据
Object.assign(formData, row)
// 立即检查车系名称是否被清空并重新设置
setTimeout(() => {
if (!formData.series_name) {
formData.series_name = row.series_name
}
}, 1)
// 先加载品牌选项
await loadBrandOptions()
// 如果品牌已选择,加载车系选项
if (row.brand_code) {
await loadSeriesOptions(row.brand_code)
} else if (row.brand_name) {
// 如果只有品牌名称没有品牌代码,尝试匹配品牌代码
const matchedBrand = brandOptions.value.find(
(b: any) => b.brand_name === row.brand_name || b.name === row.brand_name
)
if (matchedBrand && matchedBrand.brand_code) {
await loadSeriesOptions(matchedBrand.brand_code)
}
}
// 如果车系已选择,加载车型选项
if (row.series_code) {
// 先尝试用原始的车系代码加载
await loadModelsOptions(row.series_code)
// 如果车型选项为空,尝试用车系名称匹配的车系代码
if (modelsOptions.value.length === 0 && row.series_name) {
const matchedSeries = seriesOptions.value.find(
(s: any) => s.series_name === row.series_name || s.name === row.series_name
)
if (matchedSeries && matchedSeries.series_code) {
await loadModelsOptions(matchedSeries.series_code)
}
}
} else if (row.series_name) {
// 如果只有车系名称没有车系代码,尝试匹配车系代码
const matchedSeries = seriesOptions.value.find(
(s: any) => s.series_name === row.series_name || s.name === row.series_name
)
if (matchedSeries && matchedSeries.series_code) {
await loadModelsOptions(matchedSeries.series_code)
}
}
// 如果仍然没有车型选项,但有车系信息,尝试再次加载
if (modelsOptions.value.length === 0 && (row.series_code || row.series_name)) {
// 使用当前表单中的车系代码重新加载
if (formData.series_code) {
await loadModelsOptions(formData.series_code)
}
}
// 确保车系代码与车系名称匹配(在选项数据加载完成后)
// 如果车系名称被清空,重新设置
if (!formData.series_name && row.series_name) {
formData.series_name = row.series_name
}
if (formData.series_name && seriesOptions.value.length > 0) {
const matchedSeries = seriesOptions.value.find(
(s: any) => s.series_name === formData.series_name || s.name === formData.series_name
)
if (matchedSeries && matchedSeries.series_code) {
formData.series_code = matchedSeries.series_code
}
}
// 确保车型信息正确回显(在选项数据加载完成后)
if (formData.model_name && modelsOptions.value.length > 0) {
const matchedModel = modelsOptions.value.find(
(m: any) => m.model_name === formData.model_name || m.name === formData.model_name
)
if (matchedModel) {
// 如果找到匹配的车型,更新车型代码、年款和指导价
formData.model_code = matchedModel.model_code || matchedModel.code || formData.model_code
formData.model_year = matchedModel.model_year || matchedModel.year || formData.model_year
formData.msrp = matchedModel.msrp || formData.msrp
}
}
// 强制触发一次车系名称监听器(在回显逻辑完成后)
const currentSeriesName = formData.series_name
if (currentSeriesName) {
formData.series_name = ''
await nextTick()
formData.series_name = currentSeriesName
}
dialogVisible.value = true
}
@@ -1128,14 +1401,19 @@
const resetForm = () => {
Object.keys(formData).forEach((key) => {
if (key === 'is_active') {
(formData as any)[key] = 1
;(formData as any)[key] = 1
} else if (key === 'currency') {
(formData as any)[key] = 'CNY'
;(formData as any)[key] = 'CNY'
} else {
(formData as any)[key] = ''
;(formData as any)[key] = ''
}
})
formRef.value?.clearValidate()
// 重置选项数据
modelsOptions.value = []
seriesOptions.value = []
brandOptions.value = []
}
// 提交表单
@@ -1169,14 +1447,120 @@
() => searchForm.keyword,
() => {
pagination.currentPage = 1
})
}
)
// 车型采集相关方法
const showCollectModelsDialog = () => {
if (!formData.series_code) {
ElMessage.warning('请先选择车系')
return
}
const matched = seriesOptions.value.find((s: any) => s.series_code === formData.series_code)
if (!matched) {
ElMessage.warning('未找到对应的车系信息')
return
}
currentCollectSeries.value = matched
collectModelsDialogVisible.value = true
collectModelsData.value = []
selectedModelId.value = ''
// 调用车型采集
collectVehicleModels(matched)
}
const collectVehicleModels = async (series: any) => {
try {
collectModelsLoading.value = true
// 调用后端接口获取车型数据
const response = await vehicleService.collectVehicleModels({ brand_series_id: series.id })
// 后端返回的数据格式为数组
collectModelsData.value = response || []
selectedModelId.value = ''
ElMessage.success(`成功采集到 ${collectModelsData.value.length} 个车型`)
} catch (error: any) {
ElMessage.error(error.message || '车型采集失败')
} finally {
collectModelsLoading.value = false
}
}
// 选择车型
const selectModel = (modelId: string) => {
selectedModelId.value = modelId
}
const saveCollectModels = async () => {
if (!currentCollectSeries.value || !selectedModelId.value) {
ElMessage.warning('请选择一个车型')
return
}
try {
// 找到选中的车型数据
const selectedModel = collectModelsData.value.find(
(model: any) => (model.car_id || model.title) === selectedModelId.value
)
if (!selectedModel) {
ElMessage.warning('未找到选中的车型数据')
return
}
await vehicleService.saveVehicleModels({
series_id: currentCollectSeries.value.id,
models: [selectedModel]
})
ElMessage.success('成功保存车型')
collectModelsDialogVisible.value = false
// 将选中的车型信息回显到表单
formData.model_name = selectedModel.title || selectedModel.model_name || ''
formData.model_code = selectedModel.car_id || selectedModel.model_code || ''
formData.model_year = selectedModel.year || ''
formData.msrp = selectedModel.msrp || ''
// 刷新车型选项
await loadModelsOptions(formData.series_code)
} catch (error: any) {
ElMessage.error(error.message || '保存车型失败')
}
}
// 格式化价格显示
const formatPrice = (price: number | string) => {
if (!price) return '0'
const numPrice = typeof price === 'string' ? parseFloat(price) : price
return numPrice.toLocaleString()
}
// 页面加载时获取数据
onMounted(async () => {
loadBrandOptions()
await getList()
})
// 品牌名称变更时,联动设置品牌代码
watch(
() => searchForm.brand_id,
(newId) => {
const matched = brandOptions.value.find((b: any) => b.id === newId)
const brand_code = matched ? matched.brand_code : ''
// 品牌变更后,清空车系相关字段并加载对应车系
searchForm.series_id = ''
if (brand_code) {
loadSeriesOptions(brand_code)
} else {
seriesOptions.value = []
}
}
)
watch(
() => formData.brand_name,
(newName) => {
@@ -1196,14 +1580,71 @@
// 车系名称变更时,联动设置车系代码
watch(
() => formData.series_name,
(newSeriesName) => {
(newSeriesName, oldSeriesName) => {
if (!newSeriesName) return
const matchedSeries = seriesOptions.value.find(
(s: any) => s.series_name === newSeriesName || s.name === newSeriesName
)
formData.series_code = matchedSeries ? matchedSeries.series_code || matchedSeries.code : ''
if (matchedSeries) {
formData.series_code = matchedSeries.series_code || matchedSeries.code || ''
// 只有在车系真正发生变化时才清空车型相关字段
if (oldSeriesName && oldSeriesName !== newSeriesName) {
formData.model_name = ''
formData.model_code = ''
}
if (formData.series_code) {
loadModelsOptions(formData.series_code)
} else {
modelsOptions.value = []
}
} else {
// 如果找不到匹配的车系,清空车系代码
formData.series_code = ''
modelsOptions.value = []
}
}
)
// 车型名称变更时,联动设置车型代码
watch(
() => formData.model_name,
(newModelName) => {
const matchedModel = modelsOptions.value.find(
(m: any) => m.model_name === newModelName || m.name === newModelName
)
formData.model_code = matchedModel ? matchedModel.model_code || matchedModel.code : ''
}
)
// 处理车型下拉选择变化
const handleModelSelectChange = (value: string) => {
// 如果是新创建的值(不在现有选项中),清空车型代码
const matchedModel = modelsOptions.value.find(
(m: any) => m.model_name === value || m.name === value
)
if (!matchedModel) {
formData.model_code = ''
// 清空年款和指导价
formData.model_year = ''
formData.msrp = ''
} else {
// 如果找到匹配的车型,更新车型代码、年款和指导价
formData.model_code = matchedModel.model_code || matchedModel.code || ''
formData.model_year = matchedModel.model_year || matchedModel.year || ''
formData.msrp = matchedModel.msrp || ''
}
}
// 处理车型直接输入变化
const handleModelInputChange = () => {
// 直接输入时清空车型代码,让后端自动生成
formData.model_code = ''
}
// 外观颜色变更时,联动设置外观颜色代码
watch(
() => formData.color_exterior_text,
@@ -1353,11 +1794,6 @@
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable {
cursor: pointer;
}
@@ -1423,4 +1859,42 @@
white-space: nowrap;
margin: 0 auto;
}
.collect-models-container {
background: transparent;
.collect-models-header {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.collect-models-content {
min-height: 400px;
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.empty-container {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.models-table-container {
.price-text {
color: var(--el-color-danger);
font-weight: 500;
}
}
}
}
</style>
+2 -2
View File
@@ -36,11 +36,11 @@
<div class="form">
<!-- 登录方式切换 tab -->
<div class="login-type-tabs">
<span
<!-- <span
:class="{ active: activeLoginType === 'password' }"
@click="activeLoginType = 'password'"
>账号密码登录</span
>
> -->
<span
:class="{ active: activeLoginType === 'qrcode' }"
@click="activeLoginType = 'qrcode'"
+125 -36
View File
@@ -1,13 +1,18 @@
<template>
<div class="stocking-show-page">
<div class="boxshadow">
<OrderDetailContent :orderData="orderDialog.orderData" />
<div v-if="flow_type == 'stock_orders' || flow_type == 'stock_orders_nullify'">
<OrderDetailContent :orderData="orderDialog.orderData" />
</div>
<div v-if="flow_type == 'purchase_orders'">
<ProcurementDetailView :detailData="orderDialog.orderData" />
</div>
</div>
<!-- 上传合同 -->
<div class="boxshadow">
<div class="form-title">
<span class="border"></span>
<span class="title">合同</span>
<span class="title">上传合同</span>
</div>
<div class="file-preview">
<div v-if="contractFile" class="contract-file">
@@ -99,6 +104,9 @@
<ElButton type="primary" size="large" class="submit-btn" @click="handleSubmit('approve')"
>同意</ElButton
>
<!-- <ElButton type="warning" size="large" class="submit-btn" @click="handleTransfer()"
>转交</ElButton
> -->
</div>
</div>
<!-- PDF 预览弹窗 -->
@@ -122,37 +130,105 @@
/>
</div>
</el-dialog>
<!-- 转交弹出框 -->
<el-dialog v-model="zjDialogVisible" title="转交" width="450px">
<el-form :model="zjForm" label-width="80px">
<el-form-item label="转交人" prop="to_uid">
<el-select
v-model="zjForm.to_uid"
filterable
placeholder="请选择转交人"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<ElFormItem label="转交原因" prop="reason">
<ElInput v-model="zjForm.reason" type="textarea" :rows="3" placeholder="请输入转交原因" />
</ElFormItem>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="zjDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handlezjSubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
// import { manageService } from '@/api/adminApi'
import { manageService } from '@/api/adminApi'
import { CrmService } from '@/api/crmApi'
import { useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { usePdfPreview } from '@/composables/usePdfPreview'
import { router } from '@/router'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import ProcurementDetailView from '@/components/custom/ProcurementDetailView.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
defineOptions({ name: 'CrmApprovalDetail' }) // 定义组件名称,用于 KeepAlive 缓存控制
import { router } from '@/router'
defineOptions({ name: 'CrmApprovalDetail' })
const { pdf, frameRef, download } = usePdfPreview()
const zjDialogVisible = ref(false)
const route = useRoute()
const approval_id = route.query.id
const stocking_id = route.query.biz_id
const instance_id = route.query.instance_id
const step_index = route.query.step_index
const status = route.query.status
const flow_type = route.query.flow_type
const approvalNotes = ref('')
const orderDialog = reactive({
orderData: {} as OrderData
})
// const approval_id = route.query.id
const status = route.query.status
const stocking_id = route.query.biz_id
const instance_id = route.query.instance_id
const stockingInfo = ref<any>({})
const approvalNotes = ref('')
const approvalAction = ref('')
const userOptions = ref<any>()
const getuserList = async () => {
// 这里调用获取用户列表的接口
const res = await manageService.presetusers()
userOptions.value = res || []
// return res || []
}
const zjForm = reactive({
to_uid: '',
instance_id: '',
step_index: '',
reason: ''
})
// 转交操作
const handleTransfer = async () => {
getuserList()
zjDialogVisible.value = true
}
const handlezjSubmit = async () => {
if (!zjForm.to_uid) {
ElMessage.warning('请选择转交人')
return
}
if (!zjForm.reason) {
ElMessage.warning('请输入转交原因')
return
}
try {
const zjForms = {
to_uid: zjForm.to_uid,
instance_id: instance_id,
step_index: step_index,
reason: zjForm.reason
}
const { message } = await CrmService.stockapprovaltransfer(zjForms)
ElMessage.success(message)
router.push('/admin/approval/stocking_auth')
} catch (error) {
console.error('转交失败:', error)
// ElMessage.error('转交失败')
}
}
const handleSubmit = async (approvalAction: string) => {
try {
// 这里调用审批API
const { message } = await CrmService.stockapprovalpass({
instance_id: instance_id,
action: approvalAction,
@@ -174,14 +250,38 @@
// 合同文件
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
function extractFileName(pathOrUrl: string | undefined | null): string {
if (!pathOrUrl) return '合同文件'
try {
const clean = String(pathOrUrl)
const afterSlash = clean.split(/[\\/]/).pop() || '合同文件'
return decodeURIComponent(afterSlash)
} catch {
return '合同文件'
}
}
//获取详情并映射到回显模型
const getdetilfn = async () => {
const res = await CrmService.checkStock({ id: stocking_id })
const orderData: OrderData = res
orderDialog.orderData = orderData
stockingInfo.value = res
const res = await CrmService.stockapprovaldetailborder({
id: approval_id,
flow_type: flow_type
})
if (flow_type == 'stock_orders') {
const orderData: OrderData = res
orderDialog.orderData = orderData
} else if (flow_type == 'purchase_orders') {
const orderData: OrderData = res
const urls = res.contract_file_url
const paths = res.contract_file
const list: Array<{ name: string; url: string }> = []
const nameFromPath = (p: string) => extractFileName(p)
urls.forEach((u: string, i: number) => {
const p = paths[i] || ''
list.push({ name: p ? nameFromPath(p) : extractFileName(u), url: u })
})
orderData.contract_file_url = list
orderDialog.orderData = orderData
}
// 合同文件
if (res?.contract) {
contractFile.value = {
@@ -447,8 +547,10 @@
}
.submit-wrap {
display: flex;
justify-content: center;
justify-content: space-around;
padding: 24px 0 8px;
width: 500px;
margin: auto;
}
/* 审批记录 时间线 */
@@ -538,17 +640,4 @@
color: #999;
padding: 8px 0 4px 4px;
}
.submit-wrap {
display: flex;
justify-content: space-around;
padding: 24px 0 8px;
width: 500px;
margin: auto;
}
.submit-btn {
width: 100px;
height: 44px;
font-size: 16px;
}
</style>
+212 -51
View File
@@ -1,9 +1,6 @@
<template>
<div class="stocking-page">
<div class="stocking-toolbar boxshadow">
<!-- <div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleCreate">创建备货单</ElButton>
</div> -->
<div class="toolbar-right">
<el-input
v-model="keyword"
@@ -32,14 +29,25 @@
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140" />
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140">
<template #default="scope">
<span :style="{ color: getFlowTypeColor(scope.row.flow_type) }">
{{ scope.row.flows_name }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
<ElTableColumn label="审批结果" prop="action_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120">
<template #default="scope">
<div style="display: flex; align-items: center">
<img class="avatar" :src="scope.row.initiator_avatar" alt="" />
<p>{{ scope.row.initiator_name }}</p>
</div>
</template>
</ElTableColumn>
<ElTableColumn label="审核人" prop="approver_name" min-width="120" />
<ElTableColumn label="审批时间" prop="approved_at" min-width="120" />
<ElTableColumn label="状态" prop="status_desc" min-width="120" />
<ElTableColumn label="创建时间" prop="created_at" min-width="120" />
<ElTableColumn label="提交时间" prop="created_at" min-width="120" />
<ElTableColumn label="操作" fixed="right" min-width="140" align="center">
<template #default="scope">
<ElRow :gutter="10" justify="center">
@@ -69,7 +77,7 @@
<!-- 快速审批弹出框 -->
<el-dialog
v-model="approvalDialogVisible"
title="合塑实业-汽车事业部-备货单(客户下定)"
:title="orderDialog.title"
width="80%"
:close-on-click-modal="false"
:close-on-press-escape="false"
@@ -77,18 +85,25 @@
>
<div class="approval-content">
<!-- 头部信息 -->
<OrderDetailContent :orderData="orderDialog.orderData" />
<div
v-if="orderDialog.type == 'stock_orders' || orderDialog.type == 'stock_orders_nullify'"
>
<OrderDetailContent :orderData="orderDialog.orderData" />
</div>
<div v-if="orderDialog.type == 'purchase_orders'">
<ProcurementDetailView :detailData="orderDialog.orderData" />
</div>
<!-- 审批备注 -->
<div class="info-section">
<ElRow :gutter="12">
<ElCol :span="24">
<el-input
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="备注"
class="approval-notes"
/>
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="备注"
class="approval-notes"
/>
</ElCol>
</ElRow>
</div>
@@ -96,39 +111,107 @@
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleApprove('reject')"
<ElButton
type="primary"
plain
size="large"
class="submit-btn"
@click="handleApprove('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleApprove('approve')"
>同意</ElButton
>
<!-- <ElButton type="warning" size="large" class="submit-btn" @click="handleTransfer()"
>转交</ElButton
> -->
</div>
</template>
</el-dialog>
<!-- 转交弹出框 -->
<el-dialog v-model="zjDialogVisible" title="转交" width="450px">
<el-form :model="zjForm" label-width="80px">
<el-form-item label="转交人" prop="to_uid">
<el-select
v-model="zjForm.to_uid"
filterable
placeholder="请选择转交人"
style="width: 100%"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<ElFormItem label="转交原因" prop="reason">
<ElInput v-model="zjForm.reason" type="textarea" :rows="3" placeholder="请输入转交原因" />
</ElFormItem>
<div style="display: flex; justify-content: space-around; margin-top: 24px">
<el-button @click="zjDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handlezjSubmit">确定</el-button>
</div>
</el-form>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { router } from '@/router'
// import { manageService } from '@/api/adminApi'
import { manageService } from '@/api/adminApi'
import { useAuth } from '@/composables/useAuth'
import { CrmService } from '@/api/crmApi'
import { commonService } from '@/api/commonApi'
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
import type { OrderData } from '@/composables/useOrderDetailDialog'
defineOptions({ name: 'CrmStockingAuth' }) // 定义组件名称,用于 KeepAlive 缓存控制
const { hasAuth } = useAuth()
import ProcurementDetailView from '@/components/custom/ProcurementDetailView.vue'
defineOptions({ name: 'CrmStockingAuth' })
const zjDialogVisible = ref(false)
const orderDialog = reactive({
orderData: {} as OrderData
orderData: {} as OrderData,
title: '',
type: ''
})
const { hasAuth } = useAuth()
const loading = ref(false)
const keyword = ref('')
const tableRef = ref()
const tableData = ref<any[]>([])
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
const userOptions = ref<any>()
const getuserList = async () => {
// 这里调用获取用户列表的接口
const res = await manageService.presetusers()
userOptions.value = res || []
// return res || []
}
// 根据 flow_type 获取审批单名称的字体颜色
const getFlowTypeColor = (flowType: string): string => {
switch (flowType) {
case 'stock_orders':
return '#409EFF' // 蓝色 - 备货单
case 'stock_orders_nullify':
return '#F56C6C' // 红色 - 备货单作废
case 'purchase_orders':
return '#67C23A' // 绿色 - 采购单
case 'purchase_orders_nullify':
return '#E6A23C' // 橙色 - 采购单作废
case 'return_orders':
return '#909399' // 灰色 - 退货单
case 'refund_orders':
return '#9C27B0' // 紫色 - 退款单
default:
return '#303133' // 默认黑色
}
}
const zjForm = reactive({
to_uid: '',
instance_id: '',
step_index: '',
reason: ''
})
// 审批弹出框相关
const approvalDialogVisible = ref(false)
@@ -174,21 +257,68 @@
const handleView = (row: any) => {
router.push({
path: '/crmpc/approval/auth_detil',
query: { id: row.id, biz_id: row.biz_id, instance_id: row.instance_id, status: row.status } // 可带客户名等参数
query: {
id: row.id,
biz_id: row.biz_id,
instance_id: row.instance_id,
status: row.status,
flow_type: row.flow_type,
step_index: row.step_index
} // 可带客户名等参数
})
}
const getDictItemSelectlist = ref<any[]>([])
const getDictItemSelect = async () => {
const res = await commonService.getDictItemSelect({ dict_id: '18' })
getDictItemSelectlist.value = res
}
const instance_id = ref('')
function extractFileName(pathOrUrl: string | undefined | null): string {
if (!pathOrUrl) return '合同文件'
try {
const clean = String(pathOrUrl)
const afterSlash = clean.split(/[\\/]/).pop() || '合同文件'
return decodeURIComponent(afterSlash)
} catch {
return '合同文件'
}
}
const activerow = ref()
const handleCreateRepair = async (row: any) => {
// getDictItemSelect()
activerow.value = row
instance_id.value = row.instance_id
const res = await CrmService.checkStock({ id: row.biz_id })
const orderData: OrderData = res
orderDialog.orderData = orderData
const res = await CrmService.stockapprovaldetailborder({
id: row.id,
flow_type: row.flow_type
})
console.log(res)
switch (row.flow_type) {
case 'stock_orders': {
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
break
}
case 'stock_orders_nullify': {
const orderData: OrderData = res
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '备货单-作废'
break
}
case 'purchase_orders': {
const orderData: OrderData = res
const urls = res.contract_file_url
const paths = res.contract_file
const list: Array<{ name: string; url: string }> = []
const nameFromPath = (p: string) => extractFileName(p)
urls.forEach((u: string, i: number) => {
const p = paths[i] || ''
list.push({ name: p ? nameFromPath(p) : extractFileName(u), url: u })
})
orderData.contract_file_url = list
orderDialog.orderData = orderData
orderDialog.type = row.flow_type
orderDialog.title = '合塑实业-汽车事业部-采购单(采购)'
break
}
}
approvalDialogVisible.value = true
}
@@ -213,15 +343,46 @@
ElMessage.error('审批失败')
}
}
// 转交操作
const handleTransfer = async () => {
zjForm.instance_id = activerow.value.instance_id
zjForm.step_index = activerow.value.step_index
getuserList()
zjDialogVisible.value = true
}
const handlezjSubmit = async () => {
if (!zjForm.to_uid) {
ElMessage.warning('请选择转交人')
return
}
if (!zjForm.reason) {
ElMessage.warning('请输入转交原因')
return
}
try {
const { message } = await CrmService.stockapprovaltransfer(zjForm)
ElMessage.success(message)
zjDialogVisible.value = false
approvalDialogVisible.value = false
approvalNotes.value = ''
zjForm.to_uid = ''
zjForm.reason = ''
instance_id.value = ''
// 刷新列表
getList()
} catch (error) {
console.error('转交失败:', error)
// ElMessage.error('转交失败')
}
}
</script>
<style scoped lang="scss">
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
.avatar {
width: 40px;
height: 40px;
border-radius: 6px;
margin-right: 10px;
}
.stocking-page {
background: var(--art-main-bg-color);
@@ -253,22 +414,22 @@
.approval-content {
max-height: 70vh;
overflow-y: auto;
}
}
.header {
display: flex;
.header {
display: flex;
justify-content: center;
align-items: center;
align-items: center;
margin-bottom: 12px;
color: #333333;
.mr40 { margin-right: 40px; }
}
.info-section {
margin-bottom: 24px;
margin: 24px 0px;
.section-title {
display: flex;
display: flex;
align-items: center;
margin-bottom: 16px;
@@ -316,20 +477,20 @@
margin-bottom: 12px;
.detail-item {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
label {
margin-bottom: 6px;
font-size: 14px;
font-weight: 500;
font-weight: 500;
color: #606266;
}
}
}
}
}
}
}
.summary-section {
display: flex;
@@ -359,10 +520,10 @@
}
.dialog-footer {
display: flex;
justify-content: space-around;
width: 500px;
margin: auto;
display: flex;
gap: 12px;
.reject-btn {
@@ -32,7 +32,13 @@
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140" />
<ElTableColumn label="审批单名称" prop="flows_name" min-width="140">
<template #default="scope">
<span :style="{ color: getFlowTypeColor(scope.row.flow_type) }">
{{ scope.row.flows_name }}
</span>
</template>
</ElTableColumn>
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
<ElTableColumn label="审批结果" prop="action_desc" min-width="160" />
<ElTableColumn label="提交人" prop="initiator_name" min-width="120" />
@@ -83,12 +89,12 @@
<ElRow :gutter="12">
<ElCol :span="24">
<el-input
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="备注"
class="approval-notes"
/>
v-model="approvalNotes"
type="textarea"
:rows="2"
placeholder="备注"
class="approval-notes"
/>
</ElCol>
</ElRow>
</div>
@@ -96,7 +102,12 @@
<template #footer>
<div class="dialog-footer">
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleApprove('reject')"
<ElButton
type="primary"
plain
size="large"
class="submit-btn"
@click="handleApprove('reject')"
>驳回</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleApprove('approve')"
@@ -136,7 +147,25 @@
onMounted(() => {
getList()
})
// 根据 flow_type 获取审批单名称的字体颜色
const getFlowTypeColor = (flowType: string): string => {
switch (flowType) {
case 'stock_orders':
return '#409EFF' // 蓝色 - 备货单
case 'stock_orders_nullify':
return '#F56C6C' // 红色 - 备货单作废
case 'purchase_orders':
return '#67C23A' // 绿色 - 采购单
case 'purchase_orders_nullify':
return '#E6A23C' // 橙色 - 采购单作废
case 'return_orders':
return '#909399' // 灰色 - 退货单
case 'refund_orders':
return '#9C27B0' // 紫色 - 退款单
default:
return '#303133' // 默认黑色
}
}
watch([keyword], () => {
pagination.currentPage = 1
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
+145 -73
View File
@@ -9,15 +9,15 @@
<span class="border"></span>
<span class="title">基础信息</span>
</div>
<ElForm :model="baseForm" class="base-form" label-position="right">
<ElForm :model="baseForm" :rules="baseFormRules" ref="baseFormRef" class="base-form" label-position="right">
<ElRow :gutter="12">
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="阿里订单号/PI号" label-width="114px">
<ElFormItem label="阿里订单号/PI号" label-width="124px" prop="pi_no" required>
<ElInput v-model="baseForm.pi_no" placeholder="请输入阿里订单号/PI号" />
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="日期" label-width="114px">
<ElFormItem label="日期" label-width="114px" prop="biz_date" required>
<ElDatePicker
v-model="baseForm.biz_date"
type="date"
@@ -29,16 +29,12 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="清关目的国" label-width="114px">
<ElFormItem label="清关目的国" label-width="114px" prop="clearance_country_id" required>
<el-select
v-model="baseForm.clearance_country_id"
filterable
placeholder="请选择国家"
@change="
(val) =>
(baseForm.clearance_country_snapshot =
(countryOptions || []).find((x: any) => x.id === val)?.cn_addrs || '')
"
@change="(val) => (baseForm.clearance_country_snapshot = (countryOptions || []).find((x: any) => x.id === val)?.cn_addrs || '')"
>
<el-option
v-for="item in countryOptions"
@@ -51,7 +47,7 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="客户名称" label-width="114px">
<ElFormItem label="客户名称" label-width="114px" prop="customer_id" required>
<ElInput
v-model="baseForm.customer_snapshot"
placeholder="请选择客户"
@@ -73,11 +69,7 @@
filterable
disabled
placeholder="请选择客户渠道"
@change="
(val) =>
(baseForm.customer_source_snapshot =
(channelOptions || []).find((x: any) => x.id === val)?.name || '')
"
@change="(val) => (baseForm.customer_source_snapshot = (channelOptions || []).find((x: any) => x.id === val)?.name || '')"
>
<el-option
v-for="item in channelOptions"
@@ -98,11 +90,7 @@
filterable
placeholder="请选择国家"
disabled
@change="
(val) =>
(baseForm.customer_country_snapshot =
(countryOptions || []).find((x: any) => x.id === val)?.cn_addrs || '')
"
@change="(val) => (baseForm.customer_country_snapshot = (countryOptions || []).find((x: any) => x.id === val)?.cn_addrs || '')"
>
<el-option
v-for="item in countryOptions"
@@ -124,11 +112,11 @@
<span class="border"></span>
<span class="title">订单详情</span>
</div>
<ElForm class="items-wrap" label-position="right">
<ElForm :model="formModel" :rules="itemRules" ref="itemsFormRef" class="items-wrap" label-position="right">
<div class="item-row form-box" v-for="(item, idx) in items" :key="idx">
<ElRow :gutter="12" class="item-row-wrap">
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="车型" label-width="85px">
<ElFormItem label="车型" label-width="85px" :prop="`items.${idx}.sku_id`" required>
<ElInput
v-model="item.model_snapshot"
placeholder="请选择车型"
@@ -143,17 +131,12 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="颜色/外饰" label-width="85px">
<ElFormItem label="颜色/外饰" label-width="85px" :prop="`items.${idx}.color_exterior_snapshot`" required>
<el-select
v-model="item.color_exterior_snapshot"
filterable
placeholder="请选择内容"
@change="
(val) =>
(item.color_exterior_code =
(wcolorOptions || []).find((x: any) => x.color_text === val)?.color_code ||
'')
"
@change="(val) => (item.color_exterior_code = (wcolorOptions || []).find((x: any) => x.color_text === val)?.color_code || '')"
>
<ElOption
v-for="item in wcolorOptions"
@@ -166,7 +149,7 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="颜色/内饰" label-width="85px">
<ElFormItem label="颜色/内饰" label-width="85px" :prop="`items.${idx}.color_interior_snapshot`" required>
<el-select
v-model="item.color_interior_snapshot"
filterable
@@ -195,12 +178,7 @@
filterable
multiple
placeholder="请选择内容"
@change="
(vals) =>
(item.modify_list = (modificationOptions || [])
.filter((x: any) => (vals || []).includes(x.id))
.map((x: any) => ({ option_id: x.id, option_name_snapshot: x.name })))
"
@change="(vals) => (item.modify_list = (modificationOptions || []).filter((x: any) => (vals || []).includes(x.id)).map((x: any) => ({ option_id: x.id, option_name_snapshot: x.name })))"
>
<el-option
v-for="item in modificationOptions"
@@ -219,10 +197,10 @@
</ElRow>
<ElRow :gutter="12" class="item-row-wrap">
<ElCol :sm="12" :md="12" :lg="6" :xl="4" v-if="(item.mileage ?? 0) <= 0">
<ElFormItem label="官方指导价" label-width="85px">
<ElFormItem label="官方指导价" label-width="95px" :prop="`items.${idx}.official_price`" required>
<ElInput v-model="item.official_price" placeholder="请输入官方指导价">
<template #suffix>
<ElSelect v-model="item.guide_price_currency" style="width: 64px; border: none">
<ElSelect v-model="item.guide_price_currency" style="width: 74px; border: none">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -236,10 +214,10 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4" v-else>
<ElFormItem label="二手挂牌价" label-width="85px">
<ElFormItem label="二手挂牌价" label-width="95px" :prop="`items.${idx}.listing_price`" required>
<ElInput v-model="item.listing_price" placeholder="请输入二手挂牌价">
<template #suffix>
<ElSelect v-model="item.listing_price_currency" style="width: 64px">
<ElSelect v-model="item.listing_price_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -256,7 +234,7 @@
<ElFormItem label="优惠" label-width="85px">
<ElInput v-model="item.discount_price" placeholder="请输入优惠额">
<template #suffix>
<ElSelect v-model="item.discount_currency" style="width: 64px">
<ElSelect v-model="item.discount_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -273,7 +251,7 @@
<ElFormItem label="优惠" label-width="85px">
<ElInput v-model="item.reduction_price" placeholder="请输入优惠">
<template #suffix>
<ElSelect v-model="item.reduction_price_currency" style="width: 64px">
<ElSelect v-model="item.reduction_price_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -292,7 +270,7 @@
<ElFormItem label="手续费" label-width="85px">
<ElInput v-model="item.handling_fee" placeholder="请输入手续费">
<template #suffix>
<ElSelect v-model="item.helper_fee_currency" style="width: 64px">
<ElSelect v-model="item.helper_fee_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -309,7 +287,7 @@
<ElFormItem label="改装费" label-width="85px">
<ElInput v-model="item.retrofit_fee" placeholder="请输入改装费">
<template #suffix>
<ElSelect v-model="item.modify_fee_currency" style="width: 64px">
<ElSelect v-model="item.modify_fee_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -326,7 +304,7 @@
<ElFormItem label="FOB段运费" label-width="85px">
<ElInput v-model="item.fob_transport_fee" placeholder="请输入FOB段运费">
<template #suffix>
<ElSelect v-model="item.fob_fee_currency" style="width: 64px">
<ElSelect v-model="item.fob_fee_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -343,7 +321,7 @@
<ElFormItem label="港杂费" label-width="85px">
<ElInput v-model="item.port_misc_fee" placeholder="请输入港杂费">
<template #suffix>
<ElSelect v-model="item.port_fee_currency" style="width: 64px">
<ElSelect v-model="item.port_fee_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -360,7 +338,7 @@
<ElFormItem label="海运费" label-width="85px">
<ElInput v-model="item.ocean_freight" placeholder="请输入海运费">
<template #suffix>
<ElSelect v-model="item.ocean_freight_currency" style="width: 64px">
<ElSelect v-model="item.ocean_freight_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -377,7 +355,7 @@
<ElFormItem label="佣金" label-width="85px">
<ElInput v-model="item.commission" placeholder="请输入佣金">
<template #suffix>
<ElSelect v-model="item.commission_currency" style="width: 64px">
<ElSelect v-model="item.commission_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -394,7 +372,7 @@
<ElFormItem label="购置税" label-width="85px">
<ElInput v-model="item.purchase_tax" placeholder="请输入购置税">
<template #suffix>
<ElSelect v-model="item.purchase_tax_currency" style="width: 64px">
<ElSelect v-model="item.purchase_tax_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -411,7 +389,7 @@
<ElFormItem label="预估退税额" label-width="85px">
<ElInput v-model="item.tax_refund" placeholder="请输入预估退税额">
<template #suffix>
<ElSelect v-model="item.tax_refund_currency" style="width: 64px">
<ElSelect v-model="item.tax_refund_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -427,10 +405,10 @@
</ElRow>
<ElRow :gutter="12" class="item-row-wrap">
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="成交价" label-width="85px">
<ElFormItem label="成交价" label-width="85px" :prop="`items.${idx}.unit_price`" required>
<ElInput v-model="item.unit_price" placeholder="请输入成交价">
<template #suffix>
<ElSelect v-model="item.unit_price_currency" style="width: 64px">
<ElSelect v-model="item.unit_price_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -447,7 +425,7 @@
<ElFormItem label="总价" label-width="85px">
<ElInput v-model="item.total_price" placeholder="请输入总价">
<template #suffix>
<ElSelect v-model="item.total_price_currency" style="width: 64px">
<ElSelect v-model="item.total_price_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -517,7 +495,7 @@
<ElFormItem label="合计到岸价" label-width="105px">
<ElInput v-model="baseForm.total_cif" placeholder="请输入合计到岸价">
<template #suffix>
<ElSelect v-model="baseForm.total_cif_currency" style="width: 64px">
<ElSelect v-model="baseForm.total_cif_currency" style="width: 74px">
<ElOption
v-for="c in currencyOptions"
:key="c.currency_code"
@@ -534,7 +512,7 @@
<ElFormItem label="合同汇率" label-width="105px">
<ElInput v-model="baseForm.contract_exchange_rate" placeholder="请输入">
<template #suffix>
<ElSelect v-model="baseForm.settlement_currency" style="width: 64px">
<ElSelect v-model="baseForm.settlement_currency" style="width: 74px">
<ElOption
@change="(val) =>(baseForm.contract_exchange_rate =(currencyOptions || []).find((x: any) => x.currency_code === val)?.exchange_rate || '') "
v-for="c in currencyOptions"
@@ -555,15 +533,15 @@
<span class="border"></span>
<span class="title">运输详情</span>
</div>
<ElForm :model="baseForm" class="foot-form" label-position="right">
<ElForm :model="baseForm" :rules="transportFormRules" ref="transportFormRef" class="foot-form" label-position="right">
<ElRow :gutter="12">
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="清关人" label-width="98px">
<ElFormItem label="清关人" label-width="98px" prop="declarant_name" required>
<ElInput v-model="baseForm.declarant_name" placeholder="请输入清关人" />
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="预计发送日" label-width="98px">
<ElFormItem label="预计发送日" label-width="98px" prop="eta_ship_date" required>
<ElDatePicker
v-model="baseForm.eta_ship_date"
value-format="YYYY-MM-DD"
@@ -575,7 +553,7 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="国内收货港" label-width="98px">
<ElFormItem label="国内收货港" label-width="98px" prop="domestic_port_id" required>
<ElSelect
v-model="baseForm.domestic_port_id"
placeholder="请选择国内收货商"
@@ -596,7 +574,7 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="运输方式" label-width="98px">
<ElFormItem label="运输方式" label-width="98px" prop="shipping_method_id" required>
<ElSelect
v-model="baseForm.shipping_method_id"
placeholder="请选择运输方式"
@@ -618,7 +596,7 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="贸易条款" label-width="98px">
<ElFormItem label="贸易条款" label-width="98px" prop="trade_term_id" required>
<ElSelect
v-model="baseForm.trade_term_id"
placeholder="请选择"
@@ -639,7 +617,7 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="清关目的港" label-width="98px">
<ElFormItem label="清关目的港" label-width="98px" prop="clearance_port_id" required>
<ElSelect
v-model="baseForm.clearance_port_id"
placeholder="请选择"
@@ -660,7 +638,7 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="6" :xl="4">
<ElFormItem label="最迟到港时间" label-width="98px">
<ElFormItem label="最迟到港时间" label-width="108px" prop="latest_arrival_date" required>
<ElDatePicker
v-model="baseForm.latest_arrival_date"
type="date"
@@ -682,15 +660,15 @@
<div class="payment-section">
<div class="payment-items">
<div v-for="(payment, idx) in paymentItems" :key="idx" class="payment-item">
<ElForm :model="payment" label-width="55px" class="payment-form" label-position="right">
<ElForm :model="payment" :rules="paymentFormRules" :ref="(el) => setPaymentFormRef(el, idx)" label-width="65px" class="payment-form" label-position="right">
<ElRow :gutter="12">
<ElCol :sm="12" :md="12" :lg="6" :xl="6">
<ElFormItem label="汇款人" label-width="55px">
<ElFormItem label="汇款人" label-width="65px" prop="remitter_name" required>
<ElInput v-model="payment.remitter_name" placeholder="请选择汇款人" />
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="4" :xl="4">
<ElFormItem label="汇款国" label-width="55px">
<ElFormItem label="汇款国" label-width="65px" prop="remit_country_id" required>
<el-select
v-model="payment.remit_country_id"
filterable
@@ -712,7 +690,7 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="4" :xl="4">
<ElFormItem label="日期" label-width="55px">
<ElFormItem label="日期" label-width="65px" prop="remit_date" required>
<ElDatePicker
v-model="payment.remit_date"
type="date"
@@ -724,7 +702,7 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="5" :xl="5">
<ElFormItem label="账户" label-width="55px">
<ElFormItem label="账户" label-width="65px" prop="account_id" required>
<ElSelect
v-model="payment.account_id"
placeholder="请选择账户"
@@ -745,12 +723,12 @@
</ElFormItem>
</ElCol>
<ElCol :sm="12" :md="12" :lg="5" :xl="5">
<ElFormItem label="金额" label-width="55px">
<ElFormItem label="金额" label-width="65px" prop="amount" required>
<ElInput v-model="payment.amount" placeholder="请输入金额">
<template #suffix>
<ElSelect
v-model="payment.currency_code"
style="width: 64px"
style="width: 74px"
@change="
(val) => {
const found = (currencyOptions || []).find(
@@ -826,10 +804,19 @@
<div class="form-card">
<!-- 底部信息 -->
<div class="submit-wrap">
<ElButton type="primary" plain size="large" class="submit-btn" @click="handleSubmit(0)"
<ElButton
type="primary"
plain
size="large"
class="submit-btn"
@click="handleSubmitWithValidation(0)"
>保存</ElButton
>
<ElButton type="primary" size="large" class="submit-btn" @click="handleSubmit(1)"
<ElButton
type="primary"
size="large"
class="submit-btn"
@click="handleSubmitWithValidation(1)"
>提交</ElButton
>
</div>
@@ -945,13 +932,75 @@
import { useUserStore } from '@/store/modules/user'
import { CrmService } from '@/api/crmApi'
import { useStockingForm } from '@/composables/useStockingForm'
import { onMounted } from 'vue'
import { onMounted, ref, computed } from 'vue'
defineOptions({ name: 'CrmStockingAdd' }) // 定义组件名称,用于 KeepAlive 缓存控制
const { getUserInfo } = useUserStore()
const route = useRoute()
const stocking_id = route.query.id
// 表单引用
const baseFormRef = ref()
const itemsFormRef = ref()
const transportFormRef = ref()
const paymentFormRefs = ref<any[]>([])
// 表单模型
const formModel = computed(() => ({ items: items.value }))
// 设置收款表单引用
const setPaymentFormRef = (el: any, idx: number) => {
if (el) {
paymentFormRefs.value[idx] = el
}
}
// 基础表单校验规则
const baseFormRules = {
pi_no: [{ required: true, message: '请输入阿里订单号/PI号', trigger: 'blur' }],
biz_date: [{ required: true, message: '请选择日期', trigger: 'change' }],
clearance_country_id: [{ required: true, message: '请选择清关目的国', trigger: 'change' }],
customer_id: [{ required: true, message: '请选择客户', trigger: 'change' }]
}
// 订单项校验规则
const itemRules = computed(() => {
const rules: any = {}
items.value.forEach((_, idx) => {
rules[`items.${idx}.sku_id`] = [{ required: true, message: '请选择车型', trigger: 'change' }]
rules[`items.${idx}.color_exterior_snapshot`] = [
{ required: true, message: '请选择颜色/外饰', trigger: 'change' }
]
rules[`items.${idx}.color_interior_snapshot`] = [
{ required: true, message: '请选择颜色/内饰', trigger: 'change' }
]
rules[`items.${idx}.official_price`] = [
{ required: true, message: '请输入官方指导价', trigger: 'blur' }
]
rules[`items.${idx}.listing_price`] = [
{ required: true, message: '请输入二手挂牌价', trigger: 'blur' }
]
rules[`items.${idx}.unit_price`] = [
{ required: true, message: '请输入成交价', trigger: 'blur' }
]
})
return rules
})
// 运输详情校验规则
const transportFormRules = {
declarant_name: [{ required: true, message: '请输入清关人', trigger: 'blur' }],
eta_ship_date: [{ required: true, message: '请选择预计发送日', trigger: 'change' }],
domestic_port_id: [{ required: true, message: '请选择国内收货港', trigger: 'change' }],
shipping_method_id: [{ required: true, message: '请选择运输方式', trigger: 'change' }],
trade_term_id: [{ required: true, message: '请选择贸易条款', trigger: 'change' }],
clearance_port_id: [{ required: true, message: '请选择清关目的港', trigger: 'change' }],
latest_arrival_date: [{ required: true, message: '请选择最迟到港时间', trigger: 'change' }]
}
// 收款信息校验规则
const paymentFormRules = {
remitter_name: [{ required: true, message: '请输入汇款人', trigger: 'blur' }],
remit_country_id: [{ required: true, message: '请选择汇款国', trigger: 'change' }],
remit_date: [{ required: true, message: '请选择汇款日期', trigger: 'change' }],
account_id: [{ required: true, message: '请选择账户', trigger: 'change' }],
amount: [{ required: true, message: '请输入金额', trigger: 'blur' }]
}
// 使用公共的备货单表单逻辑
const {
// 数据
@@ -1024,6 +1073,29 @@
populateFromDetail(res)
}
// 自定义提交方法,添加表单校验
const handleSubmitWithValidation = async (isFinal: number) => {
try {
// 校验基础表单
await baseFormRef.value?.validate()
// 校验订单项表单
await itemsFormRef.value?.validate()
// 校验运输详情表单
await transportFormRef.value?.validate()
// 校验收款信息表单
for (let i = 0; i < paymentItems.value.length; i++) {
const paymentFormRef = paymentFormRefs.value[i]
if (paymentFormRef) {
await paymentFormRef.validate()
}
}
// 如果校验通过,调用原始的handleSubmit方法
await handleSubmit(isFinal)
} catch (error) {
console.log('表单校验失败:', error)
}
}
// 初始化表单
onMounted(async () => {
await initializeForm()
+110 -5
View File
@@ -95,10 +95,11 @@
<el-link
v-if="
scope.row.is_use == 0 &&
scope.row.approval_status == 1 &&(scope.row.appro_status?scope.row.appro_status.status=='approved':false)
scope.row.approval_status == 2 &&(scope.row.appro_status?scope.row.appro_status.status=='approved':false)
"
type="primary"
underline="never"
style="margin-right: 8px"
@click="handleCreateRepair(scope.row)"
>
<i class="iconfont-sys">&#xe6bf;</i> 创建终版
@@ -107,16 +108,54 @@
v-if="scope.row.is_use == 1"
type="success"
underline="never"
style="margin-right: 8px"
@click="handleView1(scope.row)"
>
<i class="iconfont-sys">&#xe76c;</i> 查看终版
</el-link>
<el-link type="warning" underline="never" @click="copys(scope.row)">
<el-link
type="warning"
underline="never"
@click="copys(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe8b7;</i> 复制
</el-link>
<el-link type="warning" underline="never" @click="showDialogtimeline(scope.row)">
<el-link
type="warning"
underline="never"
@click="showDialogtimeline(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe728;</i> 查看审批
</el-link>
<el-link
v-if="scope.row.approval_status == 1"
type="primary"
underline="never"
@click="stockrecallfn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe65e;</i> 撤回
</el-link>
<el-link
v-if="scope.row.approval_status == 2"
type="danger"
underline="never"
@click="stockinvalidfn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe80a;</i> 作废
</el-link>
<el-link
v-if="scope.row.approval_status == 3 || scope.row.approval_status == 4"
type="danger"
underline="never"
@click="stockdeletefn(scope.row)"
style="margin-right: 8px"
>
<i class="iconfont-sys">&#xe783;</i> 删除
</el-link>
</ElRow>
</template>
</ElTableColumn>
@@ -262,7 +301,7 @@
getList()
})
//审批进度
const showDialogtimeline = async (row: any) => {
const showDialogtimeline = async (row: any) => {
// 审批记录(兼容多种字段)
const resstockapprovallogs = await manageService.stockapprovaltimeline({
id: row.id,
@@ -284,7 +323,7 @@
timelineDialogVisible.value = true
}
function statusClass(status: any) {
if (status === 'rejected') return 'status-lost'
if (status === 'rejected' || status === 'voided') return 'status-lost'
if (status === 'approved') return 'status-following'
return 'status-dealed'
}
@@ -531,6 +570,72 @@
})
// 添加具体的复制逻辑
}
//备货单撤回
const stockrecallfn = (row: any) => {
ElMessageBox.confirm('确定要撤回此备货单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
CrmService.stockrecall({ id: row.id })
.then((res) => {
ElMessage.success(res?.message || '操作成功')
getList()
})
.catch((err) => {
console.log(err)
})
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
//备货单作废
const stockinvalidfn = (row: any) => {
ElMessageBox.confirm('确定要作废此备货单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
CrmService.stockinvalid({ id: row.id })
.then((res) => {
ElMessage.success(res?.message || '操作成功')
getList()
})
.catch((err) => {
console.log(err)
})
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
//删除
const stockdeletefn = (row: any) => {
ElMessageBox.confirm('确定要删除此备货单吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
CrmService.stockdelete({ id: row.id })
.then((res) => {
ElMessage.success(res?.message || '操作成功')
getList()
})
.catch((err) => {
console.log(err)
})
// 你可以在这里调用接口或做后续处理
})
.catch(() => {
// 用户点击了取消
})
}
</script>
<style scoped lang="scss">
File diff suppressed because it is too large Load Diff
+362
View File
@@ -0,0 +1,362 @@
<template>
<div class="port-page">
<div class="port-toolbar boxshadow">
<div class="toolbar-left">
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('port@add')" type="primary"
>创建港口</ElButton
>
</div>
<div class="toolbar-right">
<el-input
v-model="keyword"
placeholder="请输入客户名称"
clearable
style="width: 260px"
@keyup.enter="getPortList"
>
<template #suffix>
<el-icon class="clickable" @click="getPortList"><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<div class="boxshadow">
<ArtTable
ref="tableRef"
row-key="id"
:loading="loading"
:data="tableData"
:currentPage="pagination.currentPage"
:pageSize="pagination.pageSize"
:total="pagination.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<!-- <ElTableColumn label="港口代码" prop="port_code" min-width="120" /> -->
<ElTableColumn label="港口名称" prop="port_name" min-width="150" />
<ElTableColumn label="英文名称" prop="port_name_en" min-width="150" />
<ElTableColumn label="国家" prop="country_id" min-width="140">
<template #default="scope">
<span>{{ scope.row.country_name }}</span>
</template>
</ElTableColumn>
<!-- <ElTableColumn label="纬度" prop="lat" min-width="100" /> -->
<!-- <ElTableColumn label="经度" prop="lng" min-width="100" /> -->
<ElTableColumn label="排序" prop="sort" width="80" />
<ElTableColumn label="状态" prop="status" width="100">
<template #default="scope">
<ElTag :type="scope.row.status ? 'success' : 'info'">{{
scope.row.status ? '启用' : '停用'
}}</ElTag>
</template>
</ElTableColumn>
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
<ElTableColumn fixed="right" label="操作" width="200px">
<template #default="scope">
<ElRow>
<el-button link @click="showDialog('edit', scope.row)" v-if="hasAuth('port@update')"
>编辑</el-button
>
<el-button
type="danger"
link
@click="deletePort(scope.row)"
v-if="hasAuth('port@destroy')"
>删除</el-button
>
</ElRow>
</template>
</ElTableColumn>
</template>
</ArtTable>
</div>
<ElDialog
v-model="dialogVisible"
:title="dialogType === 'add' ? '新增港口' : '编辑港口'"
align-center
width="720px"
>
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="form">
<ElFormItem label="港口代码" prop="port_code">
<ElInput v-model="form.port_code" placeholder="请输入港口代码" />
</ElFormItem>
<ElFormItem label="港口名称" prop="port_name">
<ElInput v-model="form.port_name" placeholder="请输入港口名称" />
</ElFormItem>
<ElFormItem label="英文名称" prop="port_name_en">
<ElInput v-model="form.port_name_en" placeholder="请输入港口英文名称" />
</ElFormItem>
<ElFormItem label="国家" prop="country_id">
<el-select
v-model="form.country_id"
filterable
placeholder="请选择国家"
style="width: 100%"
>
<el-option
v-for="item in countryOptions"
:key="item.id"
:label="item.cn_addr"
:value="item.id"
/>
</el-select>
</ElFormItem>
<!-- <ElFormItem label="纬度" prop="lat">
<ElInput v-model="form.lat" placeholder="请输入纬度" />
</ElFormItem>
<ElFormItem label="经度" prop="lng">
<ElInput v-model="form.lng" placeholder="请输入经度" />
</ElFormItem> -->
<ElFormItem label="排序" prop="sort">
<ElInputNumber v-model="form.sort" :min="0" :max="9999" style="width: 100%" />
</ElFormItem>
<ElFormItem label="港口描述" prop="description">
<ElInput
v-model="form.description"
type="textarea"
:rows="3"
maxlength="300"
show-word-limit
placeholder="请输入港口描述"
/>
</ElFormItem>
<ElFormItem label="状态">
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
</ElFormItem>
</ElForm>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { vehicleService } from '@/api/crmpc/vehicleApi'
import { commonService } from '@/api/commonApi'
import { useAuth } from '@/composables/useAuth'
const { hasAuth } = useAuth()
defineOptions({ name: 'CrmVehiclePort' })
const loading = ref(false)
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const formRef = ref<FormInstance>()
const keyword = ref<string>('')
const countryFilter = ref<number | undefined>(undefined)
const statusFilter = ref<number | undefined>(undefined)
const countryOptions = ref<any[]>([])
const loadCountryOptions = async () => {
try {
const res = await commonService.getCountrySelect()
countryOptions.value = res || []
} catch (e) {
countryOptions.value = []
}
}
const form = reactive<any>({
id: '',
status: 1,
sort: 0,
port_code: '',
port_name: '',
port_name_en: '',
lat: '',
lng: '',
description: '',
country_id: undefined
})
const rules = reactive<FormRules>({
port_code: [{ required: true, message: '请输入港口代码', trigger: 'blur' }],
port_name: [{ required: true, message: '请输入港口名称', trigger: 'blur' }],
country_id: [{ required: true, message: '请选择国家', trigger: 'change' }]
})
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
const tableData = ref<any[]>([])
const tableRef = ref()
onMounted(() => {
loadCountryOptions()
getPortList()
})
watch([keyword, countryFilter, statusFilter], () => {
pagination.currentPage = 1
getPortList()
})
const getPortList = async () => {
loading.value = true
try {
const { currentPage, pageSize } = pagination
const params = {
current: currentPage,
size: pageSize,
keyword: keyword.value
}
const { records, current, size, total } = await vehicleService.getPortList(params)
tableData.value = records || []
Object.assign(pagination, { currentPage: current, pageSize: size, total })
} catch (error) {
console.error('获取港口列表失败:', error)
ElMessage.error('获取港口列表失败')
} finally {
loading.value = false
}
}
const handleSizeChange = (newPageSize: number) => {
pagination.pageSize = newPageSize
getPortList()
}
const handleCurrentChange = (newCurrentPage: number) => {
pagination.currentPage = newCurrentPage
getPortList()
}
const showDialog = (type: 'add' | 'edit', row?: any) => {
dialogVisible.value = true
dialogType.value = type
if (type === 'edit' && row) {
Object.assign(form, {
id: row.id,
status: row.status,
sort: row.sort ?? 0,
port_code: row.port_code,
port_name: row.port_name,
port_name_en: row.port_name_en,
lat: row.lat,
lng: row.lng,
description: row.description,
country_id: row.country_id
})
} else {
Object.assign(form, {
id: '',
status: 1,
sort: 0,
port_code: '',
port_name: '',
port_name_en: '',
lat: '',
lng: '',
description: '',
country_id: undefined
})
}
}
const deletePort = (row: any) => {
ElMessageBox.confirm('确定删除该港口吗?', '删除确认', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'error'
}).then(async () => {
try {
const { message } = await vehicleService.portDelete({ id: row.id })
ElMessage.success(message)
getPortList()
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
}
})
}
const handleSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate(async (valid) => {
if (!valid) return
try {
if (dialogType.value === 'add') {
const { message } = await vehicleService.portAdd({
status: form.status,
sort: form.sort,
port_code: form.port_code,
port_name: form.port_name,
port_name_en: form.port_name_en,
lat: form.lat,
lng: form.lng,
description: form.description,
country_id: form.country_id
})
ElMessage.success(message)
} else {
const { message } = await vehicleService.portEdit({
id: form.id,
status: form.status,
sort: form.sort,
port_code: form.port_code,
port_name: form.port_name,
port_name_en: form.port_name_en,
lat: form.lat,
lng: form.lng,
description: form.description,
country_id: form.country_id
})
ElMessage.success(message)
}
dialogVisible.value = false
getPortList()
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
}
})
}
const getCountryLabel = (id: number | undefined) => {
if (!id) return '-'
const found = countryOptions.value.find((c: any) => c.id === id)
return found ? found.name : id
}
</script>
<style lang="scss" scoped>
:deep(.el-table__empty-block) {
height: 100% !important;
min-height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
.port-page {
background: var(--art-main-bg-color);
min-height: 100vh;
}
.port-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 20px 20px;
background: var(--art-main-bg-color);
}
.port-search-group {
display: flex;
gap: 12px;
align-items: center;
}
.form {
padding: 20px 0;
}
.dialog-footer {
text-align: right;
}
.el-col2 {
margin-bottom: 16px;
}
</style>
+541 -67
View File
@@ -3,25 +3,46 @@
<!-- 搜索工具栏 -->
<div class="search-toolbar boxshadow">
<div class="toolbar-left">
<ElButton type="primary" v-ripple @click="handleAdd">新增车辆</ElButton>
</div>
<div class="toolbar-right">
<el-input
v-model="searchForm.keyword"
placeholder="请输入关键字"
clearable
style="width: 300px"
@keyup.enter="getList"
>
<template #suffix>
<el-icon class="clickable" @click="getList"><Search /></el-icon>
</template>
</el-input>
<el-form-item label="关键字:" label-width="70px">
<el-input
v-model="searchForm.keyword"
placeholder="请输入关键字"
clearable
@keyup.enter="getList"
>
</el-input>
</el-form-item>
<el-form-item label="品牌:" label-width="60px">
<ElSelect v-model="searchForm.brand_id" filterable placeholder="请选择品牌">
<ElOption label="全部" value="" />
<ElOption
v-for="item in brandOptions"
:key="item.brand_code || item.code"
:label="item.brand_name || item.name"
:value="item.id"
/>
</ElSelect>
</el-form-item>
<el-form-item label="车系:" label-width="60px">
<ElSelect v-model="searchForm.series_id" filterable placeholder="请选择车系">
<ElOption label="全部" value="" />
<ElOption
v-for="s in seriesOptions"
:key="s.series_code || s.code"
:label="s.series_name || s.name"
:value="s.id"
/>
</ElSelect>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList" v-ripple>搜索</el-button>
</el-form-item>
</div>
</div>
<!-- 数据表格 -->
<div class="table-container boxshadow">
<ElButton type="primary" v-ripple @click="handleAdd">新增车辆</ElButton>
<ArtTable
ref="tableRef"
row-key="id"
@@ -34,12 +55,12 @@
@current-change="handleCurrentChange"
>
<template #default>
<el-table-column prop="id" label="编码" min-width="170" show-overflow-tooltip align="center">
<!-- <el-table-column prop="id" label="编码" min-width="170" show-overflow-tooltip align="center">
<template #default="scope">
<div class="ellipsis" :title="scope.row.fingerprint">指纹:{{ scope.row.fingerprint }}</div>
<div class="ellipsis" :title="scope.row.sku_code">sku_code:{{ scope.row.sku_code }}</div>
</template>
</el-table-column>
</el-table-column> -->
<ElTableColumn label="车型" prop="model_name" min-width="150" align="center">
<template #default="scope">
<span class="primary">{{ scope.row.model_name }}</span>
@@ -48,7 +69,7 @@
<ElTableColumn label="品牌" prop="brand_name" min-width="100" align="center" />
<ElTableColumn label="车系" prop="series_name" min-width="120" align="center" />
<ElTableColumn label="年款" prop="model_year" align="center" />
<ElTableColumn label="版型" prop="trim" align="center" />
<!-- <ElTableColumn label="版型" prop="trim" align="center" />
<ElTableColumn label="配置等级" prop="trim_level" align="center" />
<ElTableColumn label="排量" prop="engine_disp_l" align="center">
<template #default="scope">
@@ -64,7 +85,7 @@
<template #default="scope">
{{ scope.row.range_km ? scope.row.range_km + 'km' : '-' }}
</template>
</ElTableColumn>
</ElTableColumn> -->
<ElTableColumn label="指导价(CNY)" prop="msrp" align="center" min-width="100">
<template #default="scope">
{{ scope.row.msrp ? scope.row.msrp : '-' }}
@@ -93,7 +114,6 @@
v-model="dialogVisible"
:title="dialogTitle"
width="1200px"
top="40px"
:close-on-click-modal="false"
:close-on-press-escape="false"
>
@@ -117,7 +137,7 @@
:value="item.brand_name || item.name"
/>
</ElSelect>
<ElInput v-model="formData.brand_code" placeholder="品牌编码" readonly disabled />
<!-- <ElInput v-model="formData.brand_code" placeholder="品牌编码" readonly disabled /> -->
</ElFormItem>
</ElCol>
<ElCol :span="8">
@@ -130,17 +150,74 @@
:value="s.series_name || s.name"
/>
</ElSelect>
<ElInput v-model="formData.series_code" placeholder="车系编码" readonly disabled />
<!-- <ElInput v-model="formData.series_code" placeholder="车系编码" readonly disabled /> -->
</ElFormItem>
</ElCol>
<ElCol :span="8">
<ElFormItem label="车型/配置名称" prop="model_name">
<div style="display: flex; align-items: center; gap: 8px">
<!-- 情况1: 有关联车型数据时显示下拉选择和采集按钮 -->
<template v-if="modelsOptions.length > 0">
<ElSelect
v-model="formData.model_name"
filterable
placeholder="请选择车型名称"
style="flex: 1"
:disabled="!formData.series_code"
allow-create
default-first-option
@change="handleModelSelectChange"
>
<ElOption
v-for="model in modelsOptions"
:key="model.model_code || model.code"
:label="model.model_name || model.name"
:value="model.model_name || model.name"
/>
</ElSelect>
<ElButton type="primary" size="small" @click="showCollectModelsDialog">
采集车型
</ElButton>
</template>
<!-- 情况2: 没有车型数据时显示采集按钮 -->
<template v-else-if="formData.series_code">
<ElInput
v-model="formData.model_name"
placeholder="请输入车型名称"
style="flex: 1"
@input="handleModelInputChange"
/>
<ElButton type="primary" size="small" @click="showCollectModelsDialog">
采集车型
</ElButton>
</template>
<!-- 情况3: 没有车系时直接输入 -->
<ElInput
v-else
v-model="formData.model_name"
placeholder="请输入车型名称"
style="flex: 1"
@input="handleModelInputChange"
/>
</div>
<!-- <ElInput
v-model="formData.model_code"
placeholder="车型编码"
readonly
disabled
style="margin-top: 8px"
/> -->
</ElFormItem>
</ElCol>
<!-- <ElCol :span="8">
<ElFormItem label="排量(L)" prop="engine_disp_l">
<ElInput v-model="formData.engine_disp_l" placeholder="请输入排量" />
<el-text class="mx-1" type="warning">如:1.5;纯电为空</el-text>
</ElFormItem>
</ElCol>
</ElCol> -->
</ElRow>
<ElRow :gutter="16">
<!-- <ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="版型/细分配置" prop="trim">
<ElSelect
@@ -208,14 +285,9 @@
<ElInput v-model="formData.edition_code" placeholder="版本编码" readonly disabled />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="车型/配置名称" prop="model_name">
<ElInput v-model="formData.model_name" placeholder="请输入车型名称" />
<el-text class="mx-1" type="warning">如:伊兰特,海狮07(不填则自动生成)</el-text>
</ElFormItem>
</ElCol>
</ElRow> -->
<!-- <ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="车型/配置代码" prop="model_code">
<ElInput v-model="formData.model_code" placeholder="请输入车型代码" />
@@ -228,7 +300,7 @@
<el-text class="mx-1" type="warning">如:EV多为1</el-text>
</ElFormItem>
</ElCol>
</ElRow>
</ElRow> -->
<ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="官方指导价" prop="msrp">
@@ -240,13 +312,13 @@
<ElInput v-model="formData.model_year" placeholder="YYYY" />
</ElFormItem>
</ElCol>
<ElCol :span="8">
<!-- <ElCol :span="8">
<ElFormItem label="最大功率(kW)" prop="engine_power_kw">
<ElInput v-model="formData.engine_power_kw" placeholder="请输入最大功率" />
</ElFormItem>
</ElCol>
</ElCol> -->
</ElRow>
<ElRow :gutter="16">
<!-- <ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="电池容量(kWh)" prop="battery_kwh">
<ElInput v-model="formData.battery_kwh" placeholder="请输入电池容量" />
@@ -262,8 +334,8 @@
<ElInput v-model="formData.doors" placeholder="请输入门数" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="16">
</ElRow> -->
<!-- <ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="是否涡轮" prop="engine_turbo">
<ElSelect v-model="formData.engine_turbo" placeholder="请选择">
@@ -282,8 +354,8 @@
<ElInput v-model="formData.seats" placeholder="请输入座位数" />
</ElFormItem>
</ElCol>
</ElRow>
<ElRow :gutter="16">
</ElRow> -->
<!-- <ElRow :gutter="16"> -->
<!-- <ElCol :span="8">
<ElFormItem label="原产国" prop="origin_country">
<ElSelect v-model="formData.origin_country" placeholder="请选择原产国" filterable>
@@ -304,7 +376,7 @@
</ElSelect>
</ElFormItem>
</ElCol> -->
<ElCol :span="8">
<!-- <ElCol :span="8">
<ElFormItem label="外观颜色" prop="color_exterior_text">
<ElSelect
v-model="formData.color_exterior_text"
@@ -327,8 +399,8 @@
disabled
/>
</ElFormItem>
</ElCol>
<ElCol :span="8">
</ElCol> -->
<!-- <ElCol :span="8">
<ElFormItem label="内饰颜色" prop="color_interior_text">
<ElSelect
v-model="formData.color_interior_text"
@@ -370,10 +442,10 @@
disabled
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElCol> -->
<!-- </ElRow> -->
<!-- <el-divider /> -->
<ElRow :gutter="16">
<!-- <ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="驱动类型" prop="drive_type">
<ElSelect v-model="formData.drive_type" placeholder="请选择驱动类型" clearable>
@@ -494,7 +566,7 @@
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElRow> -->
<ElRow :gutter="16">
<ElCol :span="8">
<ElFormItem label="状态" prop="is_active">
@@ -852,18 +924,83 @@
</div>
</div>
</ElDialog>
<!-- 车型采集弹出层 -->
<ElDialog
v-model="collectModelsDialogVisible"
:title="`${currentCollectSeries?.series_name} - 车型采集`"
width="900px"
align-center
>
<div class="collect-models-container">
<div class="collect-models-header">
<div
style="
display: flex;
align-items: center;
color: var(--el-text-color-secondary);
font-size: 14px;
"
>
请选择一个车型
</div>
</div>
<div class="collect-models-content">
<div v-if="collectModelsLoading" class="loading-container">
<ElLoading text="正在采集车型数据..." />
</div>
<div v-else-if="collectModelsData.length === 0" class="empty-container">
<ElEmpty description="暂无车型数据" />
</div>
<div v-else class="models-table-container">
<ElTable :data="collectModelsData" style="width: 100%" max-height="400">
<ElTableColumn width="60" align="center">
<template #default="scope">
<ElRadio
:model-value="selectedModelId"
:value="scope.row.car_id || scope.row.title"
@change="selectModel(scope.row.car_id || scope.row.title)"
/>
</template>
</ElTableColumn>
<ElTableColumn prop="brand_name" label="品牌" width="100" />
<ElTableColumn prop="series_name" label="车系" width="120" />
<ElTableColumn prop="title" label="车型名称" min-width="200" show-overflow-tooltip />
<ElTableColumn prop="year" label="年款" width="80" />
<ElTableColumn prop="msrp" label="指导价" width="120">
<template #default="scope">
<span v-if="scope.row.msrp" class="price-text">
¥{{ formatPrice(scope.row.msrp) }}
</span>
<span v-else>-</span>
</template>
</ElTableColumn>
</ElTable>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="collectModelsDialogVisible = false">取消</ElButton>
<ElButton type="primary" @click="saveCollectModels" :disabled="!selectedModelId">
保存车型
</ElButton>
</div>
</template>
</ElDialog>
</div>
</template>
<script setup lang="ts">
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
import { vehicleService } from '@/api/crmpc/vehicleApi'
import { commonService } from '@/api/commonApi'
defineOptions({ name: 'CrmVehicleList' })
defineOptions({ name: 'AdminVehicleList' })
// 搜索表单
const searchForm = reactive({
keyword: ''
keyword: '',
brand_id: '',
series_id: ''
})
// 分页
@@ -947,6 +1084,16 @@
// 车系选项
const seriesOptions = ref<Array<any>>([])
// 车型选项
const modelsOptions = ref<Array<any>>([])
// 车型采集弹出层相关
const collectModelsDialogVisible = ref(false)
const currentCollectSeries = ref<any | null>(null)
const collectModelsLoading = ref(false)
const collectModelsData = ref<Array<any>>([])
const selectedModelId = ref<string>('')
// 外观颜色
const wcolorOptions = ref<Array<any>>([])
// 内饰颜色
@@ -992,6 +1139,30 @@
ElMessage.error('获取车系列表失败')
}
}
// 获取车型列表
const loadModelsOptions = async (seriesCode?: string) => {
if (!seriesCode) {
modelsOptions.value = []
return
}
try {
const matched = seriesOptions.value.find((s: any) => s.series_code === seriesCode)
const seriesId = matched ? matched.id : ''
if (!seriesId) {
modelsOptions.value = []
return
}
const response = await commonService.getModelsSelect({
series_id: seriesId
})
modelsOptions.value = response
} catch (error) {
console.error('获取车型列表失败:', error)
ElMessage.error('获取车型列表失败')
}
}
//颜色下拉列表
const loadColorOptions = async () => {
try {
@@ -1036,7 +1207,9 @@
const params = {
current: currentPage,
size: pageSize,
keyword: searchForm.keyword
keyword: searchForm.keyword,
brand_id: searchForm.brand_id,
series_id: searchForm.series_id
}
const { records, current, size, total } = await vehicleService.getVehicleList(params)
@@ -1062,22 +1235,122 @@
}
// 新增车辆
const handleAdd = () => {
loadBrandOptions()
loadColorOptions()
loadDictOptions()
const handleAdd = async () => {
dialogTitle.value = '新增车辆'
resetForm()
await loadBrandOptions()
loadColorOptions()
loadDictOptions()
dialogVisible.value = true
}
// 编辑车辆
const handleEdit = (row: any) => {
loadBrandOptions()
const handleEdit = async (row: any) => {
loadColorOptions()
loadDictOptions()
dialogTitle.value = '编辑车辆'
// 先清空表单和选项数据,避免监听器干扰
resetForm()
// 使用 nextTick 确保 DOM 更新后再设置数据
await nextTick()
// 设置表单数据
Object.assign(formData, row)
// 立即检查车系名称是否被清空并重新设置
setTimeout(() => {
if (!formData.series_name) {
formData.series_name = row.series_name
}
}, 1)
// 先加载品牌选项
await loadBrandOptions()
// 如果品牌已选择,加载车系选项
if (row.brand_code) {
await loadSeriesOptions(row.brand_code)
} else if (row.brand_name) {
// 如果只有品牌名称没有品牌代码,尝试匹配品牌代码
const matchedBrand = brandOptions.value.find(
(b: any) => b.brand_name === row.brand_name || b.name === row.brand_name
)
if (matchedBrand && matchedBrand.brand_code) {
await loadSeriesOptions(matchedBrand.brand_code)
}
}
// 如果车系已选择,加载车型选项
if (row.series_code) {
// 先尝试用原始的车系代码加载
await loadModelsOptions(row.series_code)
// 如果车型选项为空,尝试用车系名称匹配的车系代码
if (modelsOptions.value.length === 0 && row.series_name) {
const matchedSeries = seriesOptions.value.find(
(s: any) => s.series_name === row.series_name || s.name === row.series_name
)
if (matchedSeries && matchedSeries.series_code) {
await loadModelsOptions(matchedSeries.series_code)
}
}
} else if (row.series_name) {
// 如果只有车系名称没有车系代码,尝试匹配车系代码
const matchedSeries = seriesOptions.value.find(
(s: any) => s.series_name === row.series_name || s.name === row.series_name
)
if (matchedSeries && matchedSeries.series_code) {
await loadModelsOptions(matchedSeries.series_code)
}
}
// 如果仍然没有车型选项,但有车系信息,尝试再次加载
if (modelsOptions.value.length === 0 && (row.series_code || row.series_name)) {
// 使用当前表单中的车系代码重新加载
if (formData.series_code) {
await loadModelsOptions(formData.series_code)
}
}
// 确保车系代码与车系名称匹配(在选项数据加载完成后)
// 如果车系名称被清空,重新设置
if (!formData.series_name && row.series_name) {
formData.series_name = row.series_name
}
if (formData.series_name && seriesOptions.value.length > 0) {
const matchedSeries = seriesOptions.value.find(
(s: any) => s.series_name === formData.series_name || s.name === formData.series_name
)
if (matchedSeries && matchedSeries.series_code) {
formData.series_code = matchedSeries.series_code
}
}
// 确保车型信息正确回显(在选项数据加载完成后)
if (formData.model_name && modelsOptions.value.length > 0) {
const matchedModel = modelsOptions.value.find(
(m: any) => m.model_name === formData.model_name || m.name === formData.model_name
)
if (matchedModel) {
// 如果找到匹配的车型,更新车型代码、年款和指导价
formData.model_code = matchedModel.model_code || matchedModel.code || formData.model_code
formData.model_year = matchedModel.model_year || matchedModel.year || formData.model_year
formData.msrp = matchedModel.msrp || formData.msrp
}
}
// 强制触发一次车系名称监听器(在回显逻辑完成后)
const currentSeriesName = formData.series_name
if (currentSeriesName) {
formData.series_name = ''
await nextTick()
formData.series_name = currentSeriesName
}
dialogVisible.value = true
}
@@ -1128,14 +1401,19 @@
const resetForm = () => {
Object.keys(formData).forEach((key) => {
if (key === 'is_active') {
(formData as any)[key] = 1
;(formData as any)[key] = 1
} else if (key === 'currency') {
(formData as any)[key] = 'CNY'
;(formData as any)[key] = 'CNY'
} else {
(formData as any)[key] = ''
;(formData as any)[key] = ''
}
})
formRef.value?.clearValidate()
// 重置选项数据
modelsOptions.value = []
seriesOptions.value = []
brandOptions.value = []
}
// 提交表单
@@ -1169,14 +1447,120 @@
() => searchForm.keyword,
() => {
pagination.currentPage = 1
})
}
)
// 车型采集相关方法
const showCollectModelsDialog = () => {
if (!formData.series_code) {
ElMessage.warning('请先选择车系')
return
}
const matched = seriesOptions.value.find((s: any) => s.series_code === formData.series_code)
if (!matched) {
ElMessage.warning('未找到对应的车系信息')
return
}
currentCollectSeries.value = matched
collectModelsDialogVisible.value = true
collectModelsData.value = []
selectedModelId.value = ''
// 调用车型采集
collectVehicleModels(matched)
}
const collectVehicleModels = async (series: any) => {
try {
collectModelsLoading.value = true
// 调用后端接口获取车型数据
const response = await vehicleService.collectVehicleModels({ brand_series_id: series.id })
// 后端返回的数据格式为数组
collectModelsData.value = response || []
selectedModelId.value = ''
ElMessage.success(`成功采集到 ${collectModelsData.value.length} 个车型`)
} catch (error: any) {
ElMessage.error(error.message || '车型采集失败')
} finally {
collectModelsLoading.value = false
}
}
// 选择车型
const selectModel = (modelId: string) => {
selectedModelId.value = modelId
}
const saveCollectModels = async () => {
if (!currentCollectSeries.value || !selectedModelId.value) {
ElMessage.warning('请选择一个车型')
return
}
try {
// 找到选中的车型数据
const selectedModel = collectModelsData.value.find(
(model: any) => (model.car_id || model.title) === selectedModelId.value
)
if (!selectedModel) {
ElMessage.warning('未找到选中的车型数据')
return
}
await vehicleService.saveVehicleModels({
series_id: currentCollectSeries.value.id,
models: [selectedModel]
})
ElMessage.success('成功保存车型')
collectModelsDialogVisible.value = false
// 将选中的车型信息回显到表单
formData.model_name = selectedModel.title || selectedModel.model_name || ''
formData.model_code = selectedModel.car_id || selectedModel.model_code || ''
formData.model_year = selectedModel.year || ''
formData.msrp = selectedModel.msrp || ''
// 刷新车型选项
await loadModelsOptions(formData.series_code)
} catch (error: any) {
ElMessage.error(error.message || '保存车型失败')
}
}
// 格式化价格显示
const formatPrice = (price: number | string) => {
if (!price) return '0'
const numPrice = typeof price === 'string' ? parseFloat(price) : price
return numPrice.toLocaleString()
}
// 页面加载时获取数据
onMounted(async () => {
loadBrandOptions()
await getList()
})
// 品牌名称变更时,联动设置品牌代码
watch(
() => searchForm.brand_id,
(newId) => {
const matched = brandOptions.value.find((b: any) => b.id === newId)
const brand_code = matched ? matched.brand_code : ''
// 品牌变更后,清空车系相关字段并加载对应车系
searchForm.series_id = ''
if (brand_code) {
loadSeriesOptions(brand_code)
} else {
seriesOptions.value = []
}
}
)
watch(
() => formData.brand_name,
(newName) => {
@@ -1196,14 +1580,71 @@
// 车系名称变更时,联动设置车系代码
watch(
() => formData.series_name,
(newSeriesName) => {
(newSeriesName, oldSeriesName) => {
if (!newSeriesName) return
const matchedSeries = seriesOptions.value.find(
(s: any) => s.series_name === newSeriesName || s.name === newSeriesName
)
formData.series_code = matchedSeries ? matchedSeries.series_code || matchedSeries.code : ''
if (matchedSeries) {
formData.series_code = matchedSeries.series_code || matchedSeries.code || ''
// 只有在车系真正发生变化时才清空车型相关字段
if (oldSeriesName && oldSeriesName !== newSeriesName) {
formData.model_name = ''
formData.model_code = ''
}
if (formData.series_code) {
loadModelsOptions(formData.series_code)
} else {
modelsOptions.value = []
}
} else {
// 如果找不到匹配的车系,清空车系代码
formData.series_code = ''
modelsOptions.value = []
}
}
)
// 车型名称变更时,联动设置车型代码
watch(
() => formData.model_name,
(newModelName) => {
const matchedModel = modelsOptions.value.find(
(m: any) => m.model_name === newModelName || m.name === newModelName
)
formData.model_code = matchedModel ? matchedModel.model_code || matchedModel.code : ''
}
)
// 处理车型下拉选择变化
const handleModelSelectChange = (value: string) => {
// 如果是新创建的值(不在现有选项中),清空车型代码
const matchedModel = modelsOptions.value.find(
(m: any) => m.model_name === value || m.name === value
)
if (!matchedModel) {
formData.model_code = ''
// 清空年款和指导价
formData.model_year = ''
formData.msrp = ''
} else {
// 如果找到匹配的车型,更新车型代码、年款和指导价
formData.model_code = matchedModel.model_code || matchedModel.code || ''
formData.model_year = matchedModel.model_year || matchedModel.year || ''
formData.msrp = matchedModel.msrp || ''
}
}
// 处理车型直接输入变化
const handleModelInputChange = () => {
// 直接输入时清空车型代码,让后端自动生成
formData.model_code = ''
}
// 外观颜色变更时,联动设置外观颜色代码
watch(
() => formData.color_exterior_text,
@@ -1353,11 +1794,6 @@
gap: 8px;
}
.toolbar-right {
display: flex;
align-items: center;
}
.clickable {
cursor: pointer;
}
@@ -1423,4 +1859,42 @@
white-space: nowrap;
margin: 0 auto;
}
.collect-models-container {
background: transparent;
.collect-models-header {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--el-border-color-lighter);
}
.collect-models-content {
min-height: 400px;
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.empty-container {
display: flex;
justify-content: center;
align-items: center;
height: 400px;
}
.models-table-container {
.price-text {
color: var(--el-color-danger);
font-weight: 500;
}
}
}
}
</style>