Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 938ef89ec0 | |||
| c7cdec27e0 | |||
| 5d906087d3 |
@@ -309,6 +309,7 @@
|
||||
"watchWithFilter": true,
|
||||
"whenever": true,
|
||||
"ElMessage": true,
|
||||
"ElTag": true
|
||||
"ElTag": true,
|
||||
"ElMessageBox": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ VITE_BASE_URL = /
|
||||
|
||||
# API 地址前缀
|
||||
# VITE_API_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
|
||||
# VITE_API_URL = http://219.157.255.213:25702
|
||||
# VITE_API_URL = http://219.157.255.213:25703
|
||||
# VITE_API_URL = https://mzl.dev.hnxinyouhe.com
|
||||
# VITE_API_URL = https://fdesk.hnxinyouhe.com
|
||||
VITE_API_URL = https://x-server.hnxinyouhe.com
|
||||
|
||||
VITE_API_URL = https://fdesk.hnxinyouhe.com
|
||||
# VITE_API_URL = https://x-server.hnxinyouhe.com
|
||||
|
||||
# VITE_API_URL_WSS = wss://mzl.dev.hnxinyouhe.com
|
||||
# VITE_API_URL_WSS = wss://fdesk.hnxinyouhe.com
|
||||
VITE_API_URL_WSS = wss://x-server.hnxinyouhe.com
|
||||
# Delete console
|
||||
VITE_DROP_CONSOLE = false
|
||||
@@ -5,7 +5,9 @@ VITE_BASE_URL = /
|
||||
|
||||
# API 地址前缀
|
||||
# VITE_API_URL = https://fdesk.hnxinyouhe.com
|
||||
# VITE_API_URL = https://mzl.dev.hnxinyouhe.com
|
||||
VITE_API_URL = https://x-server.hnxinyouhe.com
|
||||
|
||||
# VITE_API_URL_WSS = wss://fdesk.hnxinyouhe.com
|
||||
VITE_API_URL_WSS = wss://x-server.hnxinyouhe.com
|
||||
# Delete console
|
||||
VITE_DROP_CONSOLE = true
|
||||
@@ -3,6 +3,7 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
*.env
|
||||
.cursorrules
|
||||
src/types/components.d.ts
|
||||
src/types/auto-imports.d.ts
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pnpm dlx commitlint --edit $1
|
||||
@@ -1 +0,0 @@
|
||||
pnpm run lint:lint-staged
|
||||
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@types/echarts": "^4.9.22",
|
||||
"@vue/reactivity": "^3.4.35",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
@@ -16,6 +17,7 @@
|
||||
"axios": "^1.7.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"echarts": "^5.4.0",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"element-plus": "^2.8.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlight.js": "^11.10.0",
|
||||
@@ -2982,6 +2984,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/echarts": {
|
||||
"version": "4.9.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/echarts/-/echarts-4.9.22.tgz",
|
||||
"integrity": "sha512-7Fo6XdWpoi8jxkwP7BARUOM7riq8bMhmsCtSG8gzUcJmFhLo387tihoBYS/y5j7jl3PENT5RxeWZdN9RiwO7HQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/zrender": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -3138,6 +3149,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
|
||||
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
|
||||
},
|
||||
"node_modules/@types/zrender": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/zrender/-/zrender-4.0.6.tgz",
|
||||
"integrity": "sha512-1jZ9bJn2BsfmYFPBHtl5o3uV+ILejAtGrDcYSpT4qaVKEI/0YY+arw3XHU04Ebd8Nca3SQ7uNcLaqiL+tTFVMg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.34.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz",
|
||||
@@ -5103,6 +5120,11 @@
|
||||
"consola": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/claygl": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/claygl/-/claygl-1.3.0.tgz",
|
||||
"integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ=="
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -6399,6 +6421,18 @@
|
||||
"zrender": "5.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts-gl": {
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/echarts-gl/-/echarts-gl-2.0.9.tgz",
|
||||
"integrity": "sha512-oKeMdkkkpJGWOzjgZUsF41DOh6cMsyrGGXimbjK2l6Xeq/dBQu4ShG2w2Dzrs/1bD27b2pLTGSaUzouY191gzA==",
|
||||
"dependencies": {
|
||||
"claygl": "^1.2.1",
|
||||
"zrender": "^5.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"echarts": "^5.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.170",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz",
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,vue}": [
|
||||
"eslint --fix --quiet",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{js,ts,mjs,mts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
@@ -48,6 +52,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@types/echarts": "^4.9.22",
|
||||
"@vue/reactivity": "^3.4.35",
|
||||
"@vueuse/core": "^11.0.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
@@ -55,6 +60,7 @@
|
||||
"axios": "^1.7.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"echarts": "^5.4.0",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"element-plus": "^2.8.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"highlight.js": "^11.10.0",
|
||||
|
||||
@@ -12,16 +12,22 @@
|
||||
import { UserService } from './api/usersApi'
|
||||
import { setThemeTransitionClass } from './utils/theme/animation'
|
||||
import { checkStorageCompatibility } from './utils/storage'
|
||||
import { mittBus } from '@/utils/sys'
|
||||
import appLogo from '@/assets/img/common/logox.png'
|
||||
const router = useRouter()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { language } = storeToRefs(userStore)
|
||||
const { language, accessToken } = storeToRefs(userStore)
|
||||
function setCompanyFromUrl() {
|
||||
const userStore = useUserStore()
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const companyid = params.get('corp_id')
|
||||
const companyabbr = params.get('corp_abbr')
|
||||
// const systemid = params.get('system_id')
|
||||
// const system_code = params.get('system_code')
|
||||
if (companyid) {
|
||||
userStore.setCompany(companyid, companyabbr || '')
|
||||
// userStore.setSystems(systemid || '', system_code || '')
|
||||
// 你也可以加 localStorage/sessionStorage 备份
|
||||
}
|
||||
}
|
||||
@@ -29,22 +35,143 @@
|
||||
zh: zh,
|
||||
en: en
|
||||
}
|
||||
const isSupported = ref(false)
|
||||
const permissionStatus = ref('default')
|
||||
// 检查浏览器支持情况
|
||||
const checkSupport = () => {
|
||||
if ('Notification' in window) {
|
||||
isSupported.value = true
|
||||
console.log('浏览器支持原生Notification API')
|
||||
} else {
|
||||
isSupported.value = false
|
||||
console.log('⚠️ 浏览器不支持原生Notification API')
|
||||
}
|
||||
}
|
||||
// 请求通知权限
|
||||
const requestPermission = async () => {
|
||||
if (!('Notification' in window)) {
|
||||
console.log('浏览器不支持原生Notification API')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const permission = await Notification.requestPermission()
|
||||
permissionStatus.value = permission
|
||||
if (permission === 'granted') {
|
||||
console.log('用户已授予通知权限')
|
||||
} else if (permission === 'denied') {
|
||||
console.log('用户拒绝了通知权限')
|
||||
} else {
|
||||
console.log('用户暂时未授予通知权限')
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`请求权限时出错:`)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示基本通知(支持自定义标题与内容)
|
||||
const showBasicNotification = (
|
||||
title: string = '基本通知',
|
||||
body: string = '这是一个使用浏览器原生API的系统通知'
|
||||
) => {
|
||||
if (!('Notification' in window)) {
|
||||
ElMessage({
|
||||
message: '浏览器不支持原生Notification API',
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
if (Notification.permission !== 'granted') {
|
||||
ElMessage({
|
||||
message: '请先授予通知权限',
|
||||
type: 'warning'
|
||||
})
|
||||
requestPermission()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const iconUrl = appLogo || '/favicon.ico'
|
||||
const notification = new Notification(title, {
|
||||
body,
|
||||
requireInteraction: true,
|
||||
icon: iconUrl
|
||||
})
|
||||
notification.onclick = () => {
|
||||
notification.close()
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
onBeforeMount(() => {
|
||||
setThemeTransitionClass(true)
|
||||
setCompanyFromUrl()
|
||||
useUserStore().restoreTokenTimer()
|
||||
})
|
||||
|
||||
// 页面刷新时重置WebSocket重连次数,同时清理事件监听
|
||||
onBeforeUnmount(() => {
|
||||
import('@/utils/sys/wsClient').then(({ resetWebSocketReconnect }) => {
|
||||
resetWebSocketReconnect()
|
||||
})
|
||||
mittBus.off('ws:message')
|
||||
})
|
||||
|
||||
const handleWsMessageNotification = (data: any) => {
|
||||
if (data && data.event === 'notify.receive') {
|
||||
const notice = data?.data?.notice_data
|
||||
const title = notice?.message?.title || '新通知'
|
||||
const rawContent = notice?.message?.content || ''
|
||||
const body = typeof rawContent === 'string' ? rawContent.replace(/<[^>]+>/g, '') : ''
|
||||
showBasicNotification(title, body)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 检查存储兼容性
|
||||
checkStorageCompatibility()
|
||||
checkSupport()
|
||||
requestPermission()
|
||||
// 提升暗黑主题下页面刷新视觉体验
|
||||
setThemeTransitionClass(false)
|
||||
// 系统升级
|
||||
systemUpgrade()
|
||||
// 获取用户信息
|
||||
getUserInfo()
|
||||
// 初始化 WebSocket
|
||||
// showEventNotification()
|
||||
if (accessToken.value) {
|
||||
import('@/utils/sys/wsClient').then(
|
||||
({ initWebSocket, resetWebSocketReconnect, getWebSocketStatus }) => {
|
||||
// 页面刷新时重置重连次数
|
||||
resetWebSocketReconnect()
|
||||
initWebSocket(accessToken.value)
|
||||
// 添加调试功能,可以在控制台查看WebSocket状态
|
||||
// @ts-ignore
|
||||
window.getWebSocketStatus = getWebSocketStatus
|
||||
// console.log('WebSocket状态查看: window.getWebSocketStatus()')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 监听ws消息,用于系统级通知
|
||||
mittBus.on('ws:message', handleWsMessageNotification)
|
||||
})
|
||||
|
||||
// 登录后,accessToken 变更时立即初始化 WebSocket(无需刷新)
|
||||
watch(accessToken, async (token) => {
|
||||
if (token) {
|
||||
const { initWebSocket, getWebSocketStatus } = await import('@/utils/sys/wsClient')
|
||||
const status = getWebSocketStatus?.()
|
||||
// 避免重复初始化:已连接或正在连接时不再初始化
|
||||
if (
|
||||
!status?.readyState ||
|
||||
(status.readyState !== WebSocket.OPEN && status.readyState !== WebSocket.CONNECTING)
|
||||
) {
|
||||
initWebSocket(token)
|
||||
}
|
||||
} else {
|
||||
// 退出登录时,关闭并重置
|
||||
const { closeWebSocket, resetWebSocketReconnect } = await import('@/utils/sys/wsClient')
|
||||
closeWebSocket()
|
||||
resetWebSocketReconnect()
|
||||
}
|
||||
})
|
||||
|
||||
// 获取用户信息
|
||||
@@ -52,6 +179,35 @@
|
||||
if (userStore.isLogin) {
|
||||
try {
|
||||
const data = await UserService.getUserInfo()
|
||||
const { systemsid } = useUserStore()
|
||||
// 排除移动端电子签名页,不做签名校验
|
||||
const currentPath = router.currentRoute.value?.path || ''
|
||||
const isOnMobileSignature = currentPath.startsWith('/auth/signature/mobile')
|
||||
if (!isOnMobileSignature) {
|
||||
if (data.user.sign_img == null || data.user.sign_img == '') {
|
||||
ElMessageBox.alert('请前往个人中心完成签名操作!', '提醒', {
|
||||
confirmButtonText: '确定',
|
||||
callback: () => {
|
||||
if (systemsid == '1') {
|
||||
router.push('/admin/user/user_info')
|
||||
} else {
|
||||
router.push('/system/user-center')
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (data.user.email == null || data.user.email == '') {
|
||||
ElMessageBox.alert('邮箱没有填写!请前往个人中心补充邮箱!', '提醒', {
|
||||
confirmButtonText: '确定',
|
||||
callback: () => {
|
||||
if (systemsid == '1') {
|
||||
router.push('/admin/user/user_info')
|
||||
} else {
|
||||
router.push('/system/user-center')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
userStore.setUserInfo(data)
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败', error)
|
||||
@@ -60,59 +216,62 @@
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.boxshadow {
|
||||
.boxshadow {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 10, 118, 0.1);
|
||||
background: var(--art-main-bg-color);
|
||||
padding: 10px;
|
||||
padding: 10px 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.border{
|
||||
}
|
||||
.border {
|
||||
width: 6px;
|
||||
height: 22px;
|
||||
border-radius: 3px;
|
||||
background: #3471FF;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.el-dialog {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.el-dialog::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: linear-gradient(180deg, #E4ECFF 0%, #FFFFFF 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.el-dialog__header,
|
||||
.el-dialog__body,
|
||||
.el-dialog__footer {
|
||||
background: transparent !important;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.el-dialog__header{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding-right: 40px;
|
||||
}
|
||||
}
|
||||
.el-dialog {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.el-dialog::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background: linear-gradient(180deg, #E4ECFF 0%, #FFFFFF 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.el-dialog__header,
|
||||
.el-dialog__body,
|
||||
.el-dialog__footer {
|
||||
background: transparent !important;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.el-dialog__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.el-dialog__title{
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.el-dialog__title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn{
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
</style>
|
||||
.el-dialog__headerbtn {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.item-row-wrap {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class financeService {
|
||||
// 收款记录-列表
|
||||
static getReceiptList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/receipt/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款记录-详情
|
||||
static getReceiptDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/receipt/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款记录-添加
|
||||
static addReceipt(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/receipt/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款记录-更新
|
||||
static updateReceipt(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/receipt/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款记录-删除
|
||||
static deleteReceipt(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/receipt/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
//付款申请-列表
|
||||
static getPaymentList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/payment/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//付款申请-详情
|
||||
static getPaymentDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/payment/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//付款申请-添加
|
||||
static addPayment(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/payment/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
//付款申请-更新
|
||||
static updatePayment(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/payment/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
//付款申请-删除
|
||||
static deletePayment(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/payment/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class kpiService {
|
||||
// 月度-业绩 列表
|
||||
static getmonthlylist(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/targets/monthly/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 月度-业绩 添加
|
||||
static addmonthly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/monthly/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 月度-业绩 删除
|
||||
static deletemonthly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/monthly/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 月度-业绩 修改
|
||||
static updatemonthly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/monthly/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 月度-业绩 详情
|
||||
static detailmonthly(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/monthly/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-业绩 列表
|
||||
static getyearlylist(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/targets/annual/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 年度-业绩 添加
|
||||
static addyearly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/annual/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 年度-业绩 删除
|
||||
static deleteyearly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/annual/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 年度-业绩 详情
|
||||
static detailyearly(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/annual/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 年度-业绩 修改
|
||||
static updateyearly(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/targets/annual/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-公司目标完成率
|
||||
static statisticstotal(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/statistics/total',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-个人销售数量排名
|
||||
static statisticsranking(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/statistics/ranking',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-业务KPI分月完成数量表
|
||||
static statisticsmonthly(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/statistics/user/month',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-个人年度目标完成度(百分比)
|
||||
static statisticspercentage(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/statistics/user/progressh',
|
||||
params
|
||||
})
|
||||
}
|
||||
//年度-个人累积完成数量(辆)
|
||||
static statisticscumulative(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/targets/statistics/user/achieved',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class purchaseService {
|
||||
// 采购单预置
|
||||
static purchaseorderpredata(params: any) {
|
||||
return request.get<Api.Common.AddResponse>({
|
||||
url: '/admin/purchase/order/predata',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 采购单列表
|
||||
static purchaseorderlist(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/purchase/order/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 采购单添加
|
||||
static purchaseorderadd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/purchase/order/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 采购单更新
|
||||
static purchaseorderupdate(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/purchase/order/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 采购单删除
|
||||
static purchaseorderdelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/purchase/order/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 采购单详情
|
||||
static purchaseorderdetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/purchase/order/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class manageService {
|
||||
//仪表盘 总
|
||||
static statisticswhole() {
|
||||
return request.get<any>({
|
||||
// url: '/admin/inquiry/statistics/whole'
|
||||
url: 'admin/crm/statistics/whole'
|
||||
})
|
||||
}
|
||||
//各渠道统计
|
||||
static channelcount() {
|
||||
return request.get<any>({
|
||||
// url: '/admin/inquiry/statistics/channel/count'
|
||||
url: '/admin/crm/statistics/channel/count'
|
||||
})
|
||||
}
|
||||
//各国家统计
|
||||
static countrycount() {
|
||||
return request.get<any>({
|
||||
// url: '/admin/inquiry/statistics/country/count'
|
||||
url: '/admin/crm/statistics/country/count'
|
||||
})
|
||||
}
|
||||
//跟单人统计
|
||||
static trackcount() {
|
||||
return request.get<any>({
|
||||
// url: '/admin/inquiry/statistics/track/count'
|
||||
url: '/admin/crm/statistics/track/count'
|
||||
})
|
||||
}
|
||||
//每日录单统计
|
||||
static daycount() {
|
||||
return request.get<any>({
|
||||
// url: '/admin/inquiry/statistics/day/count'
|
||||
url: '/admin/crm/statistics/day/count'
|
||||
})
|
||||
}
|
||||
//每日录单统计
|
||||
static inquirycount(params: any) {
|
||||
return request.get<any>({
|
||||
// url: '/admin/inquiry/statistics/inquiry/count',
|
||||
url: '/admin/crm/statistics/customer/count',
|
||||
params
|
||||
})
|
||||
}
|
||||
//询单列表
|
||||
static getinquiryList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/inquiry/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 获取详情
|
||||
static inquirydetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/inquiry/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//添加询单
|
||||
static inquiryAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/inquiry/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//修改询单
|
||||
static inquiryedit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/inquiry/update',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//删除询单
|
||||
static inquirydelete(params: any) {
|
||||
return request.get<Api.Common.AddResponse>({
|
||||
url: '/admin/inquiry/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
//分配询单
|
||||
static inquiryallocation(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/admin/inquiry/allocation',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//重新分配询单
|
||||
static inquiryreallocation(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/admin/inquiry/reallocation',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
|
||||
//发布询单
|
||||
static inquiryrelease(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/admin/inquiry/release',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//获取用户下拉框
|
||||
static presetusers() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/users'
|
||||
})
|
||||
}
|
||||
//crm 客户管理
|
||||
//客户列表
|
||||
static getcrmCustomerList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListDatad>({
|
||||
url: '/admin/crm/customer/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//客户导出
|
||||
static crmCustomerExport(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/crm/customer/export',
|
||||
params,
|
||||
responseType: 'blob' // 设置响应类型为 blob
|
||||
})
|
||||
}
|
||||
//客户审核
|
||||
static crmCustomerAudit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/crm/customer/audit',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//客户转移
|
||||
static crmCustomerTransfer(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/crm/customer/transfer',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//用户列表
|
||||
static getUserList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/user/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//用户编辑
|
||||
static userEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/user/update',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//用户设置角色
|
||||
static userSetRole(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/user/authorize',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//用户菜单权限设置
|
||||
static userMenuAuth(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/user/set/resources',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//清除缓存
|
||||
static clearCache() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/cache/clear'
|
||||
})
|
||||
}
|
||||
//公海客户分配
|
||||
static crmCustomerAllocation(params: any) {
|
||||
return request.get<Api.Common.AddResponse>({
|
||||
url: '/admin/crm/customer/cede',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//公海客户跟进记录
|
||||
static crmCustomerFollow(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/crm/customer/follow',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
//公海客户删除
|
||||
static crmCustomerDelete(params: any) {
|
||||
return request.get<Api.Common.AddResponse>({
|
||||
url: '/admin/crm/customer/delete',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//回收站 恢复
|
||||
static customerRestore(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/crm/customer/restore',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单审核列表
|
||||
static getstocklist(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/approval/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单审核
|
||||
static stockapproval(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/approval/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
// 审核更新
|
||||
static stockapprovalupdate(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/approval/update',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//审核详情
|
||||
static stockapprovaldetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/approval/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审核删除
|
||||
static stockapprovaldelete(params: any) {
|
||||
return request.get<Api.Common.AddResponse>({
|
||||
url: '/admin/approval/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批记录
|
||||
static stockapprovalrecord(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/approval/records/todo',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批通过/拒绝
|
||||
static stockapprovalpass(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/approval/records/action',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批记录
|
||||
static stockapprovallogs(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/approval/records/logs',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单
|
||||
static getStockList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/stock/order/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//添加备货单
|
||||
static addStock(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/admin/stock/order/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//详情备货单
|
||||
static getStockDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/stock/order/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单查看
|
||||
static checkStock(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/stock/order/detail',
|
||||
params
|
||||
})
|
||||
}
|
||||
//打印
|
||||
static printStock(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/stock/order/print',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审核详情
|
||||
static stockapprovaldetailborder(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/approval/records/border',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批转交
|
||||
static stockapprovaltransfer(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/approval/records/transfer',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批时间轴
|
||||
static stockapprovaltimeline(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/approval/records/timeline',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class commonService {
|
||||
//通知列表
|
||||
static getNoticeList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/common/notice/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//通知详情
|
||||
static getNoticeDetail(params: any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/common/notice/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//通知已读
|
||||
static readNotice(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/notice/read',
|
||||
params
|
||||
})
|
||||
}
|
||||
//通知删除
|
||||
static delNotice(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/common/notice/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
//通知未读列表
|
||||
static getUnReadNoticeList() {
|
||||
return request.get<any>({
|
||||
url: '/common/notice/unread'
|
||||
})
|
||||
}
|
||||
//货币下拉列表
|
||||
static getCurrencySelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/currency'
|
||||
})
|
||||
}
|
||||
|
||||
//国家下拉列表
|
||||
static getCountrySelect() {
|
||||
return request.get<any>({
|
||||
url: '/crm/customer/getCountry'
|
||||
})
|
||||
}
|
||||
//品牌下拉列表
|
||||
static getBrandSelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/brand'
|
||||
})
|
||||
}
|
||||
//车系下拉列表
|
||||
static getSeriesSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/brand/series',
|
||||
params
|
||||
})
|
||||
}
|
||||
//港口下拉列表
|
||||
static getPortSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/port',
|
||||
params
|
||||
})
|
||||
}
|
||||
//颜色下拉列表
|
||||
static getColorSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/color',
|
||||
params
|
||||
})
|
||||
}
|
||||
//改装下拉列表
|
||||
static getModificationSelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/modification'
|
||||
})
|
||||
}
|
||||
//字典下拉框
|
||||
static getDictSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/dict/scope',
|
||||
params
|
||||
})
|
||||
}
|
||||
//字典项下拉框
|
||||
static getDictItemSelect(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/dictItem',
|
||||
params
|
||||
})
|
||||
}
|
||||
//字典分类下拉框
|
||||
static getDictTypeSelect() {
|
||||
return request.get<any>({
|
||||
url: '/admin/dict/group'
|
||||
})
|
||||
}
|
||||
//账户下拉框
|
||||
static getAccountSelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/account'
|
||||
})
|
||||
}
|
||||
//名下客户下拉框
|
||||
static getCustomerSelect() {
|
||||
return request.get<any>({
|
||||
url: '/crm/customer/preset'
|
||||
})
|
||||
}
|
||||
//角色下拉框
|
||||
static getRoleSelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/roles'
|
||||
})
|
||||
}
|
||||
//用户下拉框
|
||||
static getUserSelect() {
|
||||
return request.get<any>({
|
||||
url: '/common/preset/users'
|
||||
})
|
||||
}
|
||||
//签名 编辑
|
||||
static signatureEdit(params: any) {
|
||||
return request.post<any>({
|
||||
url: '/admin/user/update/sign',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,19 @@ export class CrmService {
|
||||
}
|
||||
//删除客户
|
||||
static getcustomerdelete(params: any) {
|
||||
return request.get<Api.Crm.CrmListData>({
|
||||
return request.get<any>({
|
||||
url: '/crm/customer/delete',
|
||||
params
|
||||
})
|
||||
}
|
||||
//标记无效
|
||||
static customerReport(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/customer/report',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//切换等级crm/customer/grade
|
||||
static customergrade(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
@@ -39,16 +47,15 @@ export class CrmService {
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
|
||||
//渠道下拉
|
||||
static getChannel() {
|
||||
return request.get<Api.Brand.BrandInfo>({
|
||||
return request.get<any>({
|
||||
url: '/crm/customer/getChannel'
|
||||
})
|
||||
}
|
||||
//国家下拉
|
||||
static getCountry() {
|
||||
return request.get<Api.Brand.BrandInfo>({
|
||||
return request.get<any>({
|
||||
url: '/crm/customer/getCountry'
|
||||
})
|
||||
}
|
||||
@@ -65,9 +72,10 @@ export class CrmService {
|
||||
})
|
||||
}
|
||||
//标签列表/crm/tagGroup/list
|
||||
static tagGrouplist() {
|
||||
static tagGrouplist(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/tagGroup/list'
|
||||
url: '/crm/tagGroup/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//更新客户标签 /crm/tagGroup/updatePivot
|
||||
@@ -99,16 +107,16 @@ export class CrmService {
|
||||
params
|
||||
})
|
||||
}
|
||||
//设置下次跟进时间 followNext crm/customer/followNext
|
||||
static followNext(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/customer/followNext',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//设置下次跟进时间 followNext crm/customer/followNext
|
||||
static followNext(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/customer/followNext',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
|
||||
//crm 个人信息修改
|
||||
//crm 个人信息修改
|
||||
static crmuseredit(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/user/edit',
|
||||
@@ -153,4 +161,128 @@ static followNext(params: any) {
|
||||
params
|
||||
})
|
||||
}
|
||||
//从属客户列表
|
||||
static getCustomercsList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/crm/customer/index',
|
||||
params
|
||||
})
|
||||
}
|
||||
//从属客户转移
|
||||
static cedeCustomer(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/customer/cede',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//所属员工
|
||||
static getCustomerUserList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/common/user/team',
|
||||
params
|
||||
})
|
||||
}
|
||||
//标记离职
|
||||
static markLeave(params: any) {
|
||||
return request.get<Api.Work.AddResponse>({
|
||||
url: '/common/user/resign',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//客户删除
|
||||
static crmCustomerDelete(params: any) {
|
||||
return request.get<Api.Common.AddResponse>({
|
||||
url: '/crm/customer/delete',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//通知客户重复
|
||||
static repeatCustomer(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/customer/distinctNotice',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//备货单
|
||||
static getStockList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/crm/stock/order/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//添加备货单
|
||||
static addStock(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/stock/order/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//详情备货单
|
||||
static getStockDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/stock/order/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//编辑备货单
|
||||
static editStock(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/stock/order/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
//备货单查看
|
||||
static checkStock(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/stock/order/detail',
|
||||
params
|
||||
})
|
||||
}
|
||||
//提交备货单
|
||||
static submitStock(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/crm/stock/order/approval',
|
||||
params
|
||||
})
|
||||
}
|
||||
//打印
|
||||
static printStock(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/stock/order/print',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批记录
|
||||
static stockapprovalrecord(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/approval/records/todo',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批通过/拒绝
|
||||
static stockapprovalpass(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/approval/records/action',
|
||||
params
|
||||
})
|
||||
}
|
||||
//审批记录
|
||||
static stockapprovallogs(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/approval/records/logs',
|
||||
params
|
||||
})
|
||||
}
|
||||
//终极备货单 提交
|
||||
static stockultimateapproval(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/stock/order/ultimate/approval',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class vehicleService {
|
||||
// 颜色管理
|
||||
static getColorList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/crm/color/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/color/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/color/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/color/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/color/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 车辆列表
|
||||
static getVehicleList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/crm/vehicle/sku/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/vehicle/sku/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/vehicle/sku/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/vehicle/sku/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/vehicle/sku/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 改装管理
|
||||
static getModificationList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/crm/modification/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/modification/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/crm/modification/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/modification/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/crm/modification/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class financeService {
|
||||
// 货币列表
|
||||
static getCurrencyList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/currency/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 货币添加
|
||||
static currencyAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/currency/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 货币详情
|
||||
static currencyDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/currency/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 货币编辑
|
||||
static currencyEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/currency/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 货币删除
|
||||
static currencyDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/currency/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款账户列表
|
||||
static getAccountList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/account/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款账户添加
|
||||
static accountAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/account/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款账户详情
|
||||
static accountDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/account/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款账户编辑
|
||||
static accountEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/account/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 收款账户删除
|
||||
static accountDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/account/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,10 @@
|
||||
import { asyncRoutes } from '@/router/routes/asyncRoutes'
|
||||
import { menuDataToRouter } from '@/router/utils/menuToRouter'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
interface MenuResponse {
|
||||
menuList: AppRouteRecord[]
|
||||
}
|
||||
// 合并方法
|
||||
function mergeRoutes(dynamicRoutes: AppRouteRecord[] = [], staticRoutes: AppRouteRecord[] = []): AppRouteRecord[] {
|
||||
// 深度合并逻辑
|
||||
const merged = [...staticRoutes]
|
||||
|
||||
dynamicRoutes.forEach(dynamicRoute => {
|
||||
const existingIndex = merged.findIndex(r => r.path === dynamicRoute.path)
|
||||
if (existingIndex >= 0) {
|
||||
// 合并children(如果存在)
|
||||
if (dynamicRoute.children) {
|
||||
merged[existingIndex].children = [
|
||||
...(merged[existingIndex].children || []),
|
||||
...dynamicRoute.children
|
||||
]
|
||||
}
|
||||
} else {
|
||||
merged.push(dynamicRoute)
|
||||
}
|
||||
})
|
||||
|
||||
return merged
|
||||
}
|
||||
// 菜单接口
|
||||
export const menuService = {
|
||||
async getMenuList(delay = 300): Promise<MenuResponse> {
|
||||
@@ -36,30 +14,27 @@ export const menuService = {
|
||||
if (!userStore.isLogin) {
|
||||
throw new Error('用户未登录')
|
||||
}
|
||||
const { menu } = await UserService.getMenuList()
|
||||
const menuData = menu
|
||||
// const userStore = useUserStore()
|
||||
// 模拟接口返回的菜单数据
|
||||
const menuData = userStore.info.menu
|
||||
// 2. 合并静态菜单
|
||||
const finalRoutes = mergeRoutes(menuData, asyncRoutes)
|
||||
// 验证菜单数据
|
||||
if (!menuData || !Array.isArray(menuData) || menuData.length === 0) {
|
||||
ElMessage.error('暂无系统菜单权限')
|
||||
return { menuList: [] }
|
||||
}
|
||||
|
||||
// console.log('menuData', menuData)
|
||||
// 处理菜单数据
|
||||
const menuList = finalRoutes.map((route: any) => menuDataToRouter(route))
|
||||
|
||||
const menuList = menuData.map((route: any) => menuDataToRouter(route))
|
||||
// 验证处理后的菜单列表
|
||||
if (!menuList || menuList.length === 0) {
|
||||
ElMessage.error('菜单数据处理失败')
|
||||
return { menuList: [] }
|
||||
}
|
||||
|
||||
// 模拟接口延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||
|
||||
return { menuList }
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取菜单失败:', error)
|
||||
throw error instanceof Error ? error : new Error('获取菜单失败')
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class systemService {
|
||||
//系统列表
|
||||
static getSystemList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/systems/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//系统添加
|
||||
static systemAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/systems/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//系统详情
|
||||
static systemDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/systems/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//系统编辑
|
||||
static systemEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/systems/update',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//系统删除
|
||||
static systemDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/systems/destroy',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//飞书应用列表
|
||||
static getFeishuAppList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/corp/feishu/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//飞书应用添加
|
||||
static feishuAppAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/corp/feishu/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//飞书应用详情
|
||||
static feishuAppDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/corp/feishu/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//飞书应用编辑
|
||||
static feishuAppEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/corp/feishu/update',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//飞书应用删除
|
||||
static feishuAppDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/corp/feishu/destroy',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//公司列表
|
||||
static getCompanyList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/corp/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//公司添加
|
||||
static companyAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/corp/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//公司详情
|
||||
static companyDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/corp/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//公司编辑
|
||||
static companyEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/corp/update',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//公司删除
|
||||
static companyDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/corp/destroy',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//角色列表
|
||||
static getRoleList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/role/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//角色添加
|
||||
static roleAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/role/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//角色详情
|
||||
static roleDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/role/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//角色编辑
|
||||
static roleEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/role/update',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//角色删除
|
||||
static roleDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/role/destroy',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//角色权限设置
|
||||
static roleAuth(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/role/menu/resource',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//角色下拉框
|
||||
static getRoleSelectList() {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/common/preset/roles'
|
||||
})
|
||||
}
|
||||
//菜单列表
|
||||
static getMenutreeList(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/resources/list/tree',
|
||||
params
|
||||
})
|
||||
}
|
||||
static getMenuList() {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/resources/list/menu'
|
||||
})
|
||||
}
|
||||
//菜单添加
|
||||
static menuAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/resources/store',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//菜单详情
|
||||
static menuDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/resources/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//菜单编辑
|
||||
static menuEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/resources/update',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//菜单删除
|
||||
static menuDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/resources/destroy',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//权限菜单
|
||||
static getPermissionMenuList(params: any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/resources/list/menu/tree',
|
||||
params
|
||||
})
|
||||
}
|
||||
//通知列表
|
||||
static getNoticeList(params: Api.Common.PaginatingParams) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/notice/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
//通知详情
|
||||
static getNoticeDetail(params: any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/notice/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
//通知删除
|
||||
static delNotice(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/notice/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
//通知推送
|
||||
static sendNotice(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/notice/push',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 字典类型列表
|
||||
static getDictTypeList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/dict/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 字典类型新增/详情/编辑/删除
|
||||
static dictTypeAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictTypeDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/dict/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictTypeEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictTypeDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 字典项列表
|
||||
static getDictItemList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/dict/item/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 字典项新增/详情/编辑/删除
|
||||
static dictItemAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/item/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictItemDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/dict/item/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictItemEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/item/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static dictItemDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/dict/item/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,12 @@ export class UserService {
|
||||
url: '/common/preset/corps'
|
||||
})
|
||||
}
|
||||
//获取系统
|
||||
static getsystemsList() {
|
||||
return request.get<Api.Brand.BrandInfo>({
|
||||
url: '/common/preset/systems'
|
||||
})
|
||||
}
|
||||
//注册
|
||||
static register(params: any) {
|
||||
return request.post<Api.Auth.registerResponse>({
|
||||
@@ -16,7 +22,7 @@ export class UserService {
|
||||
})
|
||||
}
|
||||
//获取 飞书信息
|
||||
static getfeishuAppList(params: { corp_id: string }) {
|
||||
static getfeishuAppList(params: any) {
|
||||
return request.get<Api.Brand.BrandInfo>({
|
||||
url: '/common/preset/feishu/app',
|
||||
params
|
||||
@@ -37,7 +43,7 @@ export class UserService {
|
||||
params
|
||||
})
|
||||
}
|
||||
//飞书凭证
|
||||
//飞书凭证
|
||||
static getfeishuscope() {
|
||||
return request.get<Api.Auth.Authfeishuscope>({
|
||||
url: '/common/auth/feishu/scope'
|
||||
@@ -66,4 +72,32 @@ export class UserService {
|
||||
params
|
||||
})
|
||||
}
|
||||
//获取菜单
|
||||
static getMenuList() {
|
||||
return request.get<Api.User.UserInfo>({
|
||||
url: '/common/preset/menus'
|
||||
})
|
||||
}
|
||||
//个人信息编辑
|
||||
static editUserInfo(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/admin/user/edit',
|
||||
params
|
||||
})
|
||||
}
|
||||
//离职分配
|
||||
static leaveUser(params: any) {
|
||||
return request.get<Api.Work.AddResponse>({
|
||||
url: '/admin/crm/customer/allocation',
|
||||
params
|
||||
})
|
||||
}
|
||||
//客户删除
|
||||
static crmCustomerDelete(params: any) {
|
||||
return request.get<Api.Common.AddResponse>({
|
||||
url: '/admin/crm/customer/delete',
|
||||
params
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
export class vehicleService {
|
||||
// 车辆品牌列表
|
||||
static getBrandList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/brand/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 车辆品牌新增/详情/编辑/删除
|
||||
static brandAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static brandDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/brand/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static brandEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static brandDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 车辆系列列表
|
||||
static getSeriesList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/brand/series/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 车辆系列新增/详情/编辑/删除
|
||||
static seriesAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/series/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static seriesDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/brand/series/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static seriesEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/series/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static seriesDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/brand/series/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 颜色管理
|
||||
static getColorList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/color/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/color/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/color/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/color/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static colorDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/color/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 改装管理
|
||||
static getModificationList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/modification/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/modification/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/modification/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/modification/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static modificationDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/modification/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 港口管理
|
||||
static getPortList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/port/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/port/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/port/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/port/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static portDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/port/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
// 车辆列表
|
||||
static getVehicleList(params: Api.Common.PaginatingParams & any) {
|
||||
return request.get<Api.Common.CommonListData>({
|
||||
url: '/admin/vehicle/sku/list',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleAdd(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/vehicle/sku/store',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleDetail(params: any) {
|
||||
return request.get<any>({
|
||||
url: '/admin/vehicle/sku/show',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleEdit(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/vehicle/sku/update',
|
||||
params
|
||||
})
|
||||
}
|
||||
static vehicleDelete(params: any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/vehicle/sku/destroy',
|
||||
params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -39,13 +39,14 @@ export class WorkService {
|
||||
// showErrorMessage: false // 不显示错误消息
|
||||
})
|
||||
}
|
||||
//工单状态下拉选择框
|
||||
static getStatus() {
|
||||
//工单状态下拉选择框
|
||||
static getStatus(params: any) {
|
||||
return request.get<Api.Brand.BrandInfo>({
|
||||
url: '/ticket/ticket/getStatus'
|
||||
url: '/ticket/ticket/getStatus',
|
||||
params
|
||||
})
|
||||
}
|
||||
//工单沟通记录
|
||||
//工单沟通记录
|
||||
static getconversationList(params: any) {
|
||||
return request.get<Api.Work.WorkListData>({
|
||||
url: '/ticket/conversation/list',
|
||||
@@ -53,7 +54,7 @@ export class WorkService {
|
||||
})
|
||||
}
|
||||
//添加工单记录 ticket/conversation/add
|
||||
static conversationAdd(params:any) {
|
||||
static conversationAdd(params: any) {
|
||||
return request.post<Api.Work.AddResponse>({
|
||||
url: '/ticket/conversation/add',
|
||||
params
|
||||
|
||||
@@ -0,0 +1,426 @@
|
||||
{
|
||||
"wordGeoCoordMap":{
|
||||
"阿富汗": [67.709953, 33.93911],
|
||||
"安哥拉": [17.873887, -11.202692],
|
||||
"阿尔巴尼亚": [20.168331, 41.153332],
|
||||
"阿拉伯联合酋长国": [53.847818, 23.424076],
|
||||
"阿根廷": [-63.61667199999999, -38.416097],
|
||||
"亚美尼亚": [45.038189, 40.069099],
|
||||
"法属南半球和南极领地": [69.348557, -49.280366],
|
||||
"澳大利亚": [133.775136, -25.274398],
|
||||
"奥地利": [14.550072, 47.516231],
|
||||
"阿塞拜疆": [47.576927, 40.143105],
|
||||
"布隆迪": [29.918886, -3.373056],
|
||||
"比利时": [4.469936, 50.503887],
|
||||
"贝宁": [2.315834, 9.30769],
|
||||
"布基纳法索": [-1.561593, 12.238333],
|
||||
"孟加拉人民共和国": [90.356331, 23.684994],
|
||||
"保加利亚": [25.48583, 42.733883],
|
||||
"巴哈马": [-77.39627999999999, 25.03428],
|
||||
"波斯尼亚和黑塞哥维那": [17.679076, 43.915886],
|
||||
"白俄罗斯": [27.953389, 53.709807],
|
||||
"伯利兹": [-88.49765, 17.189877],
|
||||
"百慕大": [-64.7505, 32.3078],
|
||||
"玻利维亚": [-63.58865299999999, -16.290154],
|
||||
"巴西": [-51.92528, -14.235004],
|
||||
"文莱": [114.727669, 4.535277],
|
||||
"不丹": [90.433601, 27.514162],
|
||||
"博茨瓦纳": [24.684866, -22.328474],
|
||||
"中非共和国": [20.939444, 6.611110999999999],
|
||||
"加拿大": [-106.346771, 56.130366],
|
||||
"瑞士": [8.227511999999999, 46.818188],
|
||||
"智利": [-71.542969, -35.675147],
|
||||
"中华人民共和国": [104.195397, 35.86166],
|
||||
"象牙海岸": [-5.547079999999999, 7.539988999999999],
|
||||
"喀麦隆": [12.354722, 7.369721999999999],
|
||||
"刚果民主共和国": [21.758664, -4.038333],
|
||||
"刚果共和国": [15.827659, -0.228021],
|
||||
"哥伦比亚": [-74.297333, 4.570868],
|
||||
"哥斯达黎加共和国": [-83.753428, 9.748916999999999],
|
||||
"古巴": [-77.781167, 21.521757],
|
||||
"北塞浦路斯": [33.429859, 35.126413],
|
||||
"塞浦路斯": [33.429859, 35.126413],
|
||||
"捷克共和国": [15.472962, 49.81749199999999],
|
||||
"德国": [10.451526, 51.165691],
|
||||
"吉布提": [42.590275, 11.825138],
|
||||
"丹麦": [9.501785, 56.26392],
|
||||
"多明尼加共和国": [-70.162651, 18.735693],
|
||||
"阿尔及利亚": [1.659626, 28.033886],
|
||||
"厄瓜多尔": [-78.18340599999999, -1.831239],
|
||||
"埃及": [30.802498, 26.820553],
|
||||
"厄立特里亚": [39.782334, 15.179384],
|
||||
"西班牙": [-3.74922, 40.46366700000001],
|
||||
"爱沙尼亚": [25.013607, 58.595272],
|
||||
"埃塞俄比亚": [40.489673, 9.145000000000001],
|
||||
"芬兰": [25.748151, 61.92410999999999],
|
||||
"斐": [178.065032, -17.713371],
|
||||
"福克兰群岛": [-59.523613, -51.796253],
|
||||
"法国": [2.213749, 46.227638],
|
||||
"加蓬共和国": [11.609444, -0.803689],
|
||||
"英国": [-3.435973, 55.378051],
|
||||
"格鲁吉亚": [-82.9000751, 32.1656221],
|
||||
"加纳": [-1.023194, 7.946527],
|
||||
"几内亚": [-9.696645, 9.945587],
|
||||
"冈比亚": [-15.310139, 13.443182],
|
||||
"几内亚比绍": [-15.180413, 11.803749],
|
||||
"赤道几内亚": [10.267895, 1.650801],
|
||||
"希腊": [21.824312, 39.074208],
|
||||
"格陵兰": [-42.604303, 71.706936],
|
||||
"危地马拉": [-90.23075899999999, 15.783471],
|
||||
"法属圭亚那": [-53.125782, 3.933889],
|
||||
"圭亚那": [-58.93018, 4.860416],
|
||||
"洪都拉斯": [-86.241905, 15.199999],
|
||||
"克罗地亚": [15.2, 45.1],
|
||||
"海地": [-72.285215, 18.971187],
|
||||
"匈牙利共和国": [19.503304, 47.162494],
|
||||
"印尼": [113.921327, -0.789275],
|
||||
"爱尔兰": [-8.24389, 53.41291],
|
||||
"伊朗": [53.688046, 32.427908],
|
||||
"伊拉克": [43.679291, 33.223191],
|
||||
"冰岛": [-19.020835, 64.963051],
|
||||
"以色列": [34.851612, 31.046051],
|
||||
"意大利": [12.56738, 41.87194],
|
||||
"牙买加": [-77.297508, 18.109581],
|
||||
"约旦": [36.238414, 30.585164],
|
||||
"日本": [138.252924, 36.204824],
|
||||
"哈萨克斯坦": [66.923684, 48.019573],
|
||||
"肯尼亚": [37.906193, -0.023559],
|
||||
"吉尔吉斯斯坦": [74.766098, 41.20438],
|
||||
"柬埔寨": [104.990963, 12.565679],
|
||||
"韩国": [127.766922, 35.907757],
|
||||
"科索沃": [20.902977, 42.6026359],
|
||||
"科威特": [47.481766, 29.31166],
|
||||
"老挝": [102.495496, 19.85627],
|
||||
"黎巴嫩": [35.862285, 33.854721],
|
||||
"利比里亚共和国": [-9.429499000000002, 6.428055],
|
||||
"利比亚": [17.228331, 26.3351],
|
||||
"斯里兰卡": [80.77179699999999, 7.873053999999999],
|
||||
"莱索托": [28.233608, -29.609988],
|
||||
"立陶宛": [23.881275, 55.169438],
|
||||
"巴林": [50.30, 26.10],
|
||||
"卢森堡": [6.129582999999999, 49.815273],
|
||||
"拉脱维亚共和国": [24.603189, 56.879635],
|
||||
"摩洛哥": [-7.092619999999999, 31.791702],
|
||||
"摩尔多瓦": [28.369885, 47.411631],
|
||||
"马达加斯加": [46.869107, -18.766947],
|
||||
"墨西哥": [-102.552784, 23.634501],
|
||||
"马其顿": [21.745275, 41.608635],
|
||||
"马里": [-3.996166, 17.570692],
|
||||
"缅甸": [95.956223, 21.913965],
|
||||
"黑山共和国": [19.37439, 42.708678],
|
||||
"蒙古": [103.846656, 46.862496],
|
||||
"莫桑比克": [35.529562, -18.665695],
|
||||
"毛里塔尼亚": [-10.940835, 21.00789],
|
||||
"马拉维": [34.301525, -13.254308],
|
||||
"马来西亚": [101.975766, 4.210484],
|
||||
"纳米比亚": [18.49041, -22.95764],
|
||||
"新喀里多尼亚": [165.618042, -20.904305],
|
||||
"尼日尔": [8.081666, 17.607789],
|
||||
"尼日利亚": [8.675277, 9.081999],
|
||||
"尼加拉瓜": [-85.207229, 12.865416],
|
||||
"荷兰": [5.291265999999999, 52.132633],
|
||||
"挪威": [8.468945999999999, 60.47202399999999],
|
||||
"尼泊尔": [84.12400799999999, 28.394857],
|
||||
"新西兰": [174.885971, -40.900557],
|
||||
"阿曼": [55.923255, 21.512583],
|
||||
"巴基斯坦": [69.34511599999999, 30.375321],
|
||||
"巴拿马共和国": [-80.782127, 8.537981],
|
||||
"秘鲁": [-75.015152, -9.189967],
|
||||
"菲律宾": [121.774017, 12.879721],
|
||||
"巴布亚新几内亚": [143.95555, -6.314992999999999],
|
||||
"波兰": [19.145136, 51.919438],
|
||||
"波多黎各": [-66.590149, 18.220833],
|
||||
"北朝鲜": [127.510093, 40.339852],
|
||||
"葡萄牙": [-8.224454, 39.39987199999999],
|
||||
"巴拉圭": [-58.443832, -23.442503],
|
||||
"卡塔尔": [51.183884, 25.354826],
|
||||
"罗马尼亚": [24.96676, 45.943161],
|
||||
"俄罗斯": [105.318756, 61.52401],
|
||||
"俄罗斯联邦": [105.318756, 61.52401],
|
||||
"卢旺达": [29.873888, -1.940278],
|
||||
"西撒哈拉": [-12.885834, 24.215527],
|
||||
"沙特阿拉伯": [45.079162, 23.885942],
|
||||
"苏丹": [30.217636, 12.862807],
|
||||
"南苏丹": [31.3069788, 6.876991899999999],
|
||||
"塞内加尔": [-14.452362, 14.497401],
|
||||
"所罗门群岛": [160.156194, -9.64571],
|
||||
"塞拉利昂": [-11.779889, 8.460555],
|
||||
"萨尔瓦多": [-88.89653, 13.794185],
|
||||
"索马里兰": [46.8252838, 9.411743399999999],
|
||||
"索马里": [46.199616, 5.152149],
|
||||
"塞尔维亚": [21.005859, 44.016521],
|
||||
"苏里南": [-56.027783, 3.919305],
|
||||
"斯洛伐克共和国": [19.699024, 48.669026],
|
||||
"斯洛文尼亚": [14.995463, 46.151241],
|
||||
"瑞典": [18.643501, 60.12816100000001],
|
||||
"斯威士兰": [31.465866, -26.522503],
|
||||
"叙利亚": [38.996815, 34.80207499999999],
|
||||
"乍得": [18.732207, 15.454166],
|
||||
"多哥": [0.824782, 8.619543],
|
||||
"泰国": [100.992541, 15.870032],
|
||||
"塔吉克斯坦共和国": [71.276093, 38.861034],
|
||||
"土库曼斯坦": [59.556278, 38.969719],
|
||||
"东帝汶": [125.727539, -8.874217],
|
||||
"特里尼达和多巴哥": [-61.222503, 10.691803],
|
||||
"突尼斯": [9.537499, 33.886917],
|
||||
"土耳其": [35.243322, 38.963745],
|
||||
"坦桑尼亚": [34.888822, -6.369028],
|
||||
"乌干达": [32.290275, 1.373333],
|
||||
"乌克兰": [31.16558, 48.379433],
|
||||
"乌拉圭": [-55.765835, -32.522779],
|
||||
"美国": [-95.712891, 37.09024],
|
||||
"乌兹别克斯坦": [64.585262, 41.377491],
|
||||
"委内瑞拉": [-66.58973, 6.42375],
|
||||
"越南": [108.277199, 14.058324],
|
||||
"瓦努阿图": [166.959158, -15.376706],
|
||||
"西岸": [35.3027226, 31.9465703],
|
||||
"也门": [48.516388, 15.552727],
|
||||
"南非": [22.937506, -30.559482],
|
||||
"赞比亚": [27.849332, -13.133897],
|
||||
"津巴布韦": [29.154857, -19.015438],
|
||||
"科摩罗": [43.872219, -11.875001],
|
||||
"印度尼西亚": [106.515414, -6.103040],
|
||||
"印度": [78.96288, 20.593684],
|
||||
"刚果": [15.12, -4.09],
|
||||
"萨摩亚独立国": [-171.46, 13.5],
|
||||
"朝鲜": [125.30, 39.09],
|
||||
"新加坡": [103.87546, 1.30243],
|
||||
"中非": [18.33, 4.21],
|
||||
"巴勒斯坦": [35.25, 31.79],
|
||||
"圣卢西亚": [-60.58, 14.02],
|
||||
"安提瓜和巴布达": [-61.48, 17.20],
|
||||
"巴巴多斯": [-59.30, 13.05],
|
||||
"开曼群岛": [-81.24, 19.20],
|
||||
"法罗群岛": [-6.56, 62.05],
|
||||
"圣文森特和格林纳丁斯": [-61.10, 13.10],
|
||||
"特立尼达和多巴哥": [-60.32, 11.18],
|
||||
"台澎金马单独关税区": [121.21, 25.02],
|
||||
"中国香港": [114.12, 22.26],
|
||||
"中国澳门": [113.5, 22.2],
|
||||
"格林纳达": [-61.4, 12.7],
|
||||
"塞舌尔共和国": [55.28, -4.4],
|
||||
"多米尼克": [61.23, 15.18],
|
||||
"斐济": [178.30, -18.06],
|
||||
"科特迪瓦": [-5.17, 6.49],
|
||||
"欧盟": [4.47, 50.50]
|
||||
},
|
||||
"nameMap":{
|
||||
"Afghanistan": "阿富汗",
|
||||
"Singapore": "新加坡",
|
||||
"Angola": "安哥拉",
|
||||
"Albania": "阿尔巴尼亚",
|
||||
"United Arab Emirates": "阿拉伯联合酋长国",
|
||||
"Argentina": "阿根廷",
|
||||
"Armenia": "亚美尼亚",
|
||||
"French Southern and Antarctic Lands": "法属南半球和南极领地",
|
||||
"Australia": "澳大利亚",
|
||||
"Austria": "奥地利",
|
||||
"Azerbaijan": "阿塞拜疆",
|
||||
"Seychelles": "塞舌尔共和国",
|
||||
"Burundi": "布隆迪",
|
||||
"Belgium": "比利时",
|
||||
"Benin": "贝宁",
|
||||
"Burkina Faso": "布基纳法索",
|
||||
"Côte d Ivoire": "科特迪瓦",
|
||||
"Bangladesh": "孟加拉人民共和国",
|
||||
"Bulgaria": "保加利亚",
|
||||
"The Bahamas": "巴哈马",
|
||||
"Bosnia and Herzegovina": "波斯尼亚和黑塞哥维那",
|
||||
"Belarus": "白俄罗斯",
|
||||
"Belize": "伯利兹",
|
||||
"Bermuda": "百慕大",
|
||||
"Bolivia": "玻利维亚",
|
||||
"Brazil": "巴西",
|
||||
"Brunei": "文莱",
|
||||
"Bhutan": "不丹",
|
||||
"Botswana": "博茨瓦纳",
|
||||
"Central African Republic": "中非共和国",
|
||||
"Canada": "加拿大",
|
||||
"Switzerland": "瑞士",
|
||||
"Chile": "智利",
|
||||
"China": "中华人民共和国",
|
||||
"Ivory Coast": "象牙海岸",
|
||||
"Cameroon": "喀麦隆",
|
||||
"Dem. Rep. Congo": "刚果民主共和国",
|
||||
"Republic of the Congo": "刚果共和国",
|
||||
"Colombia": "哥伦比亚",
|
||||
"Costa Rica": "哥斯达黎加共和国",
|
||||
"Cuba": "古巴",
|
||||
"Northern Cyprus": "北塞浦路斯",
|
||||
"Cyprus": "塞浦路斯",
|
||||
"Czech Republic": "捷克共和国",
|
||||
"Germany": "德国",
|
||||
"Djibouti": "吉布提",
|
||||
"Denmark": "丹麦",
|
||||
"Dominican Republic": "多明尼加共和国",
|
||||
"Algeria": "阿尔及利亚",
|
||||
"Ecuador": "厄瓜多尔",
|
||||
"Egypt": "埃及",
|
||||
"Eritrea": "厄立特里亚",
|
||||
"Spain": "西班牙",
|
||||
"Estonia": "爱沙尼亚",
|
||||
"Ethiopia": "埃塞俄比亚",
|
||||
"Bahrain": "巴林",
|
||||
"Finland": "芬兰",
|
||||
"Fiji": "斐济",
|
||||
"Falkland Islands": "福克兰群岛",
|
||||
"France": "法国",
|
||||
"Gabon": "加蓬共和国",
|
||||
"United Kingdom": "英国",
|
||||
"Georgia": "格鲁吉亚",
|
||||
"Ghana": "加纳",
|
||||
"Guinea": "几内亚",
|
||||
"Gambia": "冈比亚",
|
||||
"Guinea Bissau": "几内亚比绍",
|
||||
"Greece": "希腊",
|
||||
"Greenland": "格陵兰",
|
||||
"Guatemala": "危地马拉",
|
||||
"French Guiana": "法属圭亚那",
|
||||
"Guyana": "圭亚那",
|
||||
"Honduras": "洪都拉斯",
|
||||
"Croatia": "克罗地亚",
|
||||
"Haiti": "海地",
|
||||
"Hungary": "匈牙利共和国",
|
||||
"Indonesia": "印度尼西亚",
|
||||
"India": "印度",
|
||||
"Ireland": "爱尔兰",
|
||||
"Iran": "伊朗",
|
||||
"Iraq": "伊拉克",
|
||||
"Iceland": "冰岛",
|
||||
"Israel": "以色列",
|
||||
"Italy": "意大利",
|
||||
"Jamaica": "牙买加",
|
||||
"Jordan": "约旦",
|
||||
"Japan": "日本",
|
||||
"Kazakhstan": "哈萨克斯坦",
|
||||
"Kenya": "肯尼亚",
|
||||
"Kyrgyzstan": "吉尔吉斯斯坦",
|
||||
"Cambodia": "柬埔寨",
|
||||
"Kosovo": "科索沃",
|
||||
"Kuwait": "科威特",
|
||||
"Laos": "老挝",
|
||||
"Lebanon": "黎巴嫩",
|
||||
"Liberia": "利比里亚共和国",
|
||||
"Libya": "利比亚",
|
||||
"Sri Lanka": "斯里兰卡",
|
||||
"Lesotho": "莱索托",
|
||||
"Lithuania": "立陶宛",
|
||||
"Luxembourg": "卢森堡",
|
||||
"Latvia": "拉脱维亚共和国",
|
||||
"Morocco": "摩洛哥",
|
||||
"Moldova": "摩尔多瓦",
|
||||
"Madagascar": "马达加斯加",
|
||||
"Mexico": "墨西哥",
|
||||
"Macedonia": "马其顿",
|
||||
"Mali": "马里",
|
||||
"Myanmar": "缅甸",
|
||||
"Montenegro": "黑山共和国",
|
||||
"Mongolia": "蒙古",
|
||||
"Mozambique": "莫桑比克",
|
||||
"Mauritania": "毛里塔尼亚",
|
||||
"Malawi": "马拉维",
|
||||
"Malaysia": "马来西亚",
|
||||
"Namibia": "纳米比亚",
|
||||
"New Caledonia": "新喀里多尼亚",
|
||||
"Niger": "尼日尔",
|
||||
"Nigeria": "尼日利亚",
|
||||
"Nicaragua": "尼加拉瓜",
|
||||
"Netherlands": "荷兰",
|
||||
"Norway": "挪威",
|
||||
"Nepal": "尼泊尔",
|
||||
"New Zealand": "新西兰",
|
||||
"Oman": "阿曼",
|
||||
"Pakistan": "巴基斯坦",
|
||||
"Panama": "巴拿马共和国",
|
||||
"Peru": "秘鲁",
|
||||
"Philippines": "菲律宾",
|
||||
"Papua New Guinea": "巴布亚新几内亚",
|
||||
"Poland": "波兰",
|
||||
"Puerto Rico": "波多黎各",
|
||||
"North Korea": "北朝鲜",
|
||||
"Portugal": "葡萄牙",
|
||||
"Paraguay": "巴拉圭",
|
||||
"Qatar": "卡塔尔",
|
||||
"Romania": "罗马尼亚",
|
||||
"Russia": "俄罗斯",
|
||||
"Rwanda": "卢旺达",
|
||||
"Western Sahara": "西撒哈拉",
|
||||
"Saudi Arabia": "沙特阿拉伯",
|
||||
"Sudan": "苏丹",
|
||||
"S. Sudan": "南苏丹",
|
||||
"Senegal": "塞内加尔",
|
||||
"Solomon Islands": "所罗门群岛",
|
||||
"Sierra Leone": "塞拉利昂",
|
||||
"El Salvador": "萨尔瓦多",
|
||||
"Somaliland": "索马里兰",
|
||||
"Somalia": "索马里",
|
||||
"Republic of Serbia": "塞尔维亚",
|
||||
"Suriname": "苏里南",
|
||||
"Slovakia": "斯洛伐克共和国",
|
||||
"Slovenia": "斯洛文尼亚",
|
||||
"Sweden": "瑞典",
|
||||
"Swaziland": "斯威士兰",
|
||||
"Syria": "叙利亚",
|
||||
"Chad": "乍得",
|
||||
"Togo": "多哥",
|
||||
"Thailand": "泰国",
|
||||
"Tajikistan": "塔吉克斯坦共和国",
|
||||
"Turkmenistan": "土库曼斯坦",
|
||||
"East Timor": "东帝汶",
|
||||
"Trinidad and Tobago": "特立尼达和多巴哥",
|
||||
"Tunisia": "突尼斯",
|
||||
"Turkey": "土耳其",
|
||||
"Uganda": "乌干达",
|
||||
"Ukraine": "乌克兰",
|
||||
"Uruguay": "乌拉圭",
|
||||
"United States": "美国",
|
||||
"Uzbekistan": "乌兹别克斯坦",
|
||||
"Venezuela": "委内瑞拉",
|
||||
"Vietnam": "越南",
|
||||
"Vanuatu": "瓦努阿图",
|
||||
"West Bank": "西岸",
|
||||
"Yemen": "也门",
|
||||
"South Africa": "南非",
|
||||
"Zambia": "赞比亚",
|
||||
"Korea": "韩国",
|
||||
"Tanzania": "坦桑尼亚",
|
||||
"Zimbabwe": "津巴布韦",
|
||||
"Congo": "刚果",
|
||||
"Central African Rep.": "中非",
|
||||
"Serbia": "塞尔维亚",
|
||||
"Bosnia and Herz.": "波黑",
|
||||
"Czech Rep.": "捷克",
|
||||
"W. Sahara": "西撒哈拉",
|
||||
"Lao PDR": "老挝",
|
||||
"Dem. Rep. Korea": "朝鲜",
|
||||
"Falkland Is.": "福克兰群岛",
|
||||
"Timor-Leste": "东帝汶",
|
||||
"Solomon Is.": "所罗门群岛",
|
||||
"Palestine": "巴勒斯坦",
|
||||
"N. Cyprus": "北塞浦路斯",
|
||||
"Aland": "奥兰群岛",
|
||||
"Fr. S. Antarctic Lands": "法属南半球和南极陆地",
|
||||
"Mauritius": "毛里求斯",
|
||||
"Comoros": "科摩罗",
|
||||
"Eq. Guinea": "赤道几内亚",
|
||||
"Guinea-Bissau": "几内亚比绍",
|
||||
"Dominican Rep.": "多米尼加共和国",
|
||||
"Saint Lucia": "圣卢西亚",
|
||||
"Dominica": "多米尼克",
|
||||
"Antigua and Barb.": "安提瓜和巴布达",
|
||||
"U.S. Virgin Is.": "美国原始岛屿",
|
||||
"Montserrat": "蒙塞拉特",
|
||||
"Grenada": "格林纳达",
|
||||
"Barbados": "巴巴多斯",
|
||||
"Samoa": "萨摩亚独立国",
|
||||
"Bahamas": "巴哈马",
|
||||
"Cayman Is.": "开曼群岛",
|
||||
"Faeroe Is.": "法罗群岛",
|
||||
"IsIe of Man": "马恩岛",
|
||||
"Malta": "马耳他共和国",
|
||||
"Jersey": "泽西",
|
||||
"Cape Verde": "佛得角共和国",
|
||||
"Turks and Caicos Is.": "特克斯和凯科斯群岛",
|
||||
"St. Vin. and Gren.": "圣文森特和格林纳丁斯"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 313 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -400,3 +400,12 @@
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
// 修复 el-table 空数据块样式
|
||||
.el-table__empty-block {
|
||||
height: 100% !important;
|
||||
min-height: 200px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
@@ -54,23 +54,33 @@
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- 通知 -->
|
||||
<div class="btn-box notice-btn" @click="visibleNotice">
|
||||
<div class="btn notice-button">
|
||||
<i
|
||||
class="iconfont-sys notice-btn"
|
||||
:class="{ 'bell-shake': isBellShaking }"
|
||||
:style="isBellShaking ? 'color: #5d87ff' : ''"
|
||||
></i
|
||||
>
|
||||
<span class="count notice-btn" v-if="isBellShaking"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-box screen-box" @click="clearCachefn">
|
||||
<div class="btn" title="清除缓存">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 全屏按钮 -->
|
||||
<div class="btn-box screen-box" @click="toggleFullScreen">
|
||||
<div
|
||||
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||
class="btn"
|
||||
:class="{ 'full-screen-btn': !isFullscreen, 'exit-full-screen-btn': isFullscreen }"
|
||||
>
|
||||
<i class="iconfont-sys">{{ isFullscreen ? '' : '' }}</i>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 通知 -->
|
||||
<!-- <div class="btn-box notice-btn" @click="visibleNotice">
|
||||
<div class="btn notice-button">
|
||||
<i class="iconfont-sys notice-btn"></i>
|
||||
<span class="count notice-btn"></span>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- 聊天 -->
|
||||
<!-- <div class="btn-box chat-btn" @click="openChat">
|
||||
<div class="btn chat-button">
|
||||
@@ -79,7 +89,7 @@
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- 语言 -->
|
||||
<div class="btn-box" v-if="showLanguage">
|
||||
<!-- <div class="btn-box" v-if="showLanguage">
|
||||
<el-dropdown @command="changeLanguage" popper-class="langDropDownStyle">
|
||||
<div class="btn language-btn">
|
||||
<i class="iconfont-sys"></i>
|
||||
@@ -98,7 +108,7 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- 设置 -->
|
||||
<!-- <div class="btn-box" @click="openSetting">
|
||||
<el-popover :visible="showSettingGuide" placement="bottom-start" :width="190" :offset="0">
|
||||
@@ -119,7 +129,7 @@
|
||||
</div> -->
|
||||
<!-- 切换主题 -->
|
||||
<div class="btn-box" @click="themeAnimation">
|
||||
<div class="btn theme-btn">
|
||||
<div class="btn theme-btn" title="切换主题">
|
||||
<i class="iconfont-sys">{{ isDark ? '' : '' }}</i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,7 +148,10 @@
|
||||
popper-style="border: 1px solid var(--art-border-dashed-color); border-radius: calc(var(--custom-radius) / 2 + 4px); padding: 5px 16px; 5px 16px;"
|
||||
>
|
||||
<template #reference>
|
||||
<img class="cover" src="@imgs/user/avatar.webp" />
|
||||
<img
|
||||
class="cover"
|
||||
:src="getUserInfo.user.avatar ? getUserInfo.user.avatar : '@imgs/user/avatar.webp'"
|
||||
/>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="user-menu-box">
|
||||
@@ -155,15 +168,19 @@
|
||||
<span class="email">{{ userInfo.user.email }}</span>
|
||||
</div>
|
||||
</div> -->
|
||||
<ul class="user-menu">
|
||||
<li @click="goPage('/system/user-center')">
|
||||
<ul class="user-menu" style="border: none">
|
||||
<li @click="gouserInfo()">
|
||||
<i class="menu-icon iconfont-sys"></i>
|
||||
<span class="menu-txt">{{ $t('topBar.user.userCenter') }}</span>
|
||||
</li>
|
||||
<li @click="goPage('/crmpc/recycle')">
|
||||
<li @click="goPage('/work/myOrder')">
|
||||
<i class="menu-icon iconfont-sys"></i>
|
||||
<span class="menu-txt">我的工单</span>
|
||||
</li>
|
||||
<!-- <li @click="goPage('/crmpc/recycle')">
|
||||
<i class="menu-icon iconfont-sys"></i>
|
||||
<span class="menu-txt">回收站</span>
|
||||
</li>
|
||||
</li> -->
|
||||
<!-- <li @click="goPage('/system/user-center')">
|
||||
<i class="menu-icon iconfont-sys"></i>
|
||||
<span class="menu-txt">{{ $t('topBar.user.userCenter') }}</span>
|
||||
@@ -176,10 +193,10 @@
|
||||
<i class="menu-icon iconfont-sys"></i>
|
||||
<span class="menu-txt">{{ $t('topBar.user.github') }}</span>
|
||||
</li> -->
|
||||
<li @click="lockScreen()">
|
||||
<!-- <li @click="lockScreen()">
|
||||
<i class="menu-icon iconfont-sys"></i>
|
||||
<span class="menu-txt">{{ $t('topBar.user.lockScreen') }}</span>
|
||||
</li>
|
||||
</li> -->
|
||||
<div class="line"></div>
|
||||
<div class="logout-btn" @click="loginOut">
|
||||
{{ $t('topBar.user.logout') }}
|
||||
@@ -208,14 +225,14 @@
|
||||
import { mittBus } from '@/utils/sys'
|
||||
import { useMenuStore } from '@/store/modules/menu'
|
||||
import AppConfig from '@/config'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { languageOptions } from '@/locales'
|
||||
const isWindows = navigator.userAgent.includes('Windows')
|
||||
const { locale } = useI18n()
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
const { getUserInfo, home_page } = useUserStore()
|
||||
const {
|
||||
showMenuButton,
|
||||
showRefreshButton,
|
||||
@@ -229,13 +246,17 @@
|
||||
tabStyle
|
||||
} = storeToRefs(settingStore)
|
||||
|
||||
const { language, getUserInfo: userInfo } = storeToRefs(userStore)
|
||||
const { language } = storeToRefs(userStore)
|
||||
|
||||
const { menuList } = storeToRefs(useMenuStore())
|
||||
|
||||
const showNotice = ref(false)
|
||||
const notice = ref(null)
|
||||
const userMenuPopover = ref()
|
||||
// 控制铃铛是否晃动
|
||||
const isBellShaking = ref(false)
|
||||
// 轮询定时器句柄,防止重复创建
|
||||
let notificationPollTimer: number | undefined
|
||||
|
||||
const isLeftMenu = computed(() => menuType.value === MenuTypeEnum.LEFT)
|
||||
const isDualMenu = computed(() => menuType.value === MenuTypeEnum.DUAL_MENU)
|
||||
@@ -257,10 +278,20 @@
|
||||
onMounted(() => {
|
||||
initLanguage()
|
||||
document.addEventListener('click', bodyCloseNotice)
|
||||
// 监听通知数据变化,控制铃铛晃动
|
||||
watchNotificationData()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', bodyCloseNotice)
|
||||
// 清理事件监听器
|
||||
mittBus.off('notification-data-changed')
|
||||
mittBus.off('ws:message')
|
||||
// 清理通知轮询
|
||||
if (notificationPollTimer) {
|
||||
clearInterval(notificationPollTimer)
|
||||
notificationPollTimer = undefined
|
||||
}
|
||||
})
|
||||
|
||||
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
|
||||
@@ -268,7 +299,14 @@
|
||||
const toggleFullScreen = () => {
|
||||
toggleFullscreen()
|
||||
}
|
||||
|
||||
const clearCachefn = () => {
|
||||
manageService.clearCache().then((res) => {
|
||||
ElMessage.success(res.data?.message || '成功!')
|
||||
// setTimeout(() => {
|
||||
window.location.reload()
|
||||
// }, 5000)
|
||||
})
|
||||
}
|
||||
const topBarWidth = (): string => {
|
||||
const { TOP, DUAL_MENU, TOP_LEFT } = MenuTypeEnum
|
||||
const { getMenuOpenWidth } = settingStore
|
||||
@@ -296,17 +334,25 @@
|
||||
const goPage = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
const toDocs = () => {
|
||||
window.open(WEB_LINKS.DOCS)
|
||||
const { systemsid } = useUserStore()
|
||||
const gouserInfo = () => {
|
||||
if (systemsid == '1') {
|
||||
router.push('/admin/user/user_info')
|
||||
} else {
|
||||
router.push('/system/user-center')
|
||||
}
|
||||
}
|
||||
|
||||
const toGithub = () => {
|
||||
window.open(WEB_LINKS.GITHUB)
|
||||
}
|
||||
// const toDocs = () => {
|
||||
// window.open(WEB_LINKS.DOCS)
|
||||
// }
|
||||
|
||||
// const toGithub = () => {
|
||||
// window.open(WEB_LINKS.GITHUB)
|
||||
// }
|
||||
|
||||
const toHome = () => {
|
||||
router.push(HOME_PAGE)
|
||||
router.push(home_page)
|
||||
}
|
||||
|
||||
const loginOut = () => {
|
||||
@@ -346,8 +392,6 @@
|
||||
if (showSettingGuide.value) {
|
||||
settingStore.hideSettingGuide()
|
||||
}
|
||||
// 打开设置引导
|
||||
// settingStore.openSettingGuide()
|
||||
}
|
||||
|
||||
const openSearchDialog = () => {
|
||||
@@ -385,9 +429,72 @@
|
||||
userMenuPopover.value.hide()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 监听通知数据变化,控制铃铛晃动
|
||||
const watchNotificationData = () => {
|
||||
// 定期检查通知数据
|
||||
// const checkNotificationData = async () => {
|
||||
// try {
|
||||
// const { commonService } = await import('@/api/commonApi')
|
||||
// const res = await commonService.getUnReadNoticeList()
|
||||
// const noticeCount = res?.list?.length || 0
|
||||
// isBellShaking.value = noticeCount > 0
|
||||
// } catch (error) {
|
||||
// console.error('获取通知数据失败:', error)
|
||||
// isBellShaking.value = false
|
||||
// }
|
||||
// }
|
||||
// // 立即检查一次
|
||||
// checkNotificationData()
|
||||
// // 每30秒检查一次通知数据,先清理旧定时器,避免重复
|
||||
// if (notificationPollTimer) {
|
||||
// clearInterval(notificationPollTimer)
|
||||
// notificationPollTimer = undefined
|
||||
// }
|
||||
// notificationPollTimer = window.setInterval(checkNotificationData, 30000)
|
||||
// 监听通知数据变化事件
|
||||
mittBus.on('notification-data-changed', (count: unknown) => {
|
||||
isBellShaking.value = typeof count === 'number' ? count > 0 : false
|
||||
})
|
||||
// 监听WebSocket消息,处理实时通知
|
||||
mittBus.on('ws:message', (data: any) => {
|
||||
if (
|
||||
data &&
|
||||
data.event &&
|
||||
(data.event.includes('notice') ||
|
||||
data.event.includes('notification') ||
|
||||
data.event.includes('message'))
|
||||
) {
|
||||
isBellShaking.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
@use './mobile';
|
||||
// 铃铛晃动动画
|
||||
.bell-shake {
|
||||
animation: bellShake 1.2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes bellShake {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
10%,
|
||||
30%,
|
||||
50%,
|
||||
70%,
|
||||
90% {
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
20%,
|
||||
40%,
|
||||
60%,
|
||||
80% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -112,11 +112,11 @@
|
||||
import { isIframe } from '@/utils/navigation'
|
||||
import { handleMenuJump } from '@/utils/navigation'
|
||||
import SidebarSubmenu from './widget/SidebarSubmenu.vue'
|
||||
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const { home_page } = useUserStore()
|
||||
const { getMenuOpenWidth, menuType, uniqueOpened, dualMenuShowText, menuOpen, getMenuTheme } =
|
||||
storeToRefs(settingStore)
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
)
|
||||
|
||||
const toHome = () => {
|
||||
router.push(HOME_PAGE)
|
||||
router.push(home_page)
|
||||
}
|
||||
|
||||
let screenWidth = 0
|
||||
|
||||
@@ -9,43 +9,46 @@
|
||||
@click.stop=""
|
||||
>
|
||||
<div class="header">
|
||||
<span class="text">{{ $t('notice.title') }}</span>
|
||||
<span class="btn">{{ $t('notice.btnRead') }}</span>
|
||||
<!-- <span class="text">{{ $t('notice.title') }}</span> -->
|
||||
<span class="text">消息</span>
|
||||
<span class="btn" @click="readAll">{{ $t('notice.btnRead') }}</span>
|
||||
</div>
|
||||
<ul class="bar">
|
||||
<li
|
||||
v-for="(item, index) in barList"
|
||||
:key="index"
|
||||
:class="{ active: barActiveIndex === index }"
|
||||
@click="changeBar(index)"
|
||||
>
|
||||
{{ item.name }} ({{ item.num }})
|
||||
{{ item.name }} ({{ noticeList.length }})
|
||||
</li>
|
||||
</ul>
|
||||
<div class="content">
|
||||
<div class="scroll">
|
||||
<!-- 通知 -->
|
||||
<ul class="notice-list" v-show="barActiveIndex === 0">
|
||||
<li v-for="(item, index) in noticeList" :key="index">
|
||||
<ul class="notice-list">
|
||||
<!-- <ul class="notice-list" v-show="barActiveIndex === 0"> -->
|
||||
<li v-for="(item, index) in noticeList" :key="index" @click="showfn(item)" v-ripple>
|
||||
<div
|
||||
class="icon"
|
||||
:style="{ background: getNoticeStyle(item.type).backgroundColor + '!important' }"
|
||||
:style="{
|
||||
background: getNoticeStyle(item.message.scene).backgroundColor + '!important'
|
||||
}"
|
||||
>
|
||||
<i
|
||||
class="iconfont-sys"
|
||||
:style="{ color: getNoticeStyle(item.type).iconColor + '!important' }"
|
||||
v-html="getNoticeStyle(item.type).icon"
|
||||
:style="{ color: getNoticeStyle(item.message.scene).iconColor + '!important' }"
|
||||
v-html="getNoticeStyle(item.message.scene).icon"
|
||||
>
|
||||
</i>
|
||||
</div>
|
||||
<div class="text">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.time }}</p>
|
||||
<h4>{{ item.message.title }}</h4>
|
||||
<p>{{ item.message.created_at }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 消息 -->
|
||||
<ul class="user-list" v-show="barActiveIndex === 1">
|
||||
<!-- <ul class="user-list" v-show="barActiveIndex === 1">
|
||||
<li v-for="(item, index) in msgList" :key="index">
|
||||
<div class="avatar">
|
||||
<img :src="item.avatar" />
|
||||
@@ -55,27 +58,27 @@
|
||||
<p>{{ item.time }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</ul> -->
|
||||
<!-- 待办 -->
|
||||
<ul class="base" v-show="barActiveIndex === 3">
|
||||
<!-- <ul class="base" v-show="barActiveIndex === 3">
|
||||
<li v-for="(item, index) in pendingList" :key="index">
|
||||
<h4>{{ item.title }}</h4>
|
||||
<p>{{ item.time }}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</ul> -->
|
||||
|
||||
<div class="empty-tips" v-show="barActiveIndex === 0 && noticeList.length === 0">
|
||||
<div class="empty-tips" v-show="noticeList.length === 0">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||
</div>
|
||||
<div class="empty-tips" v-show="barActiveIndex === 1 && msgList.length === 0">
|
||||
<!-- <div class="empty-tips" v-show="barActiveIndex === 1 && msgList.length === 0">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||
</div>
|
||||
<div class="empty-tips" v-show="barActiveIndex === 2 && pendingList.length === 0">
|
||||
<i class="iconfont-sys"></i>
|
||||
<p>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="btn-wrapper">
|
||||
<el-button class="view-all" @click="handleViewAll" v-ripple>
|
||||
@@ -86,6 +89,29 @@
|
||||
|
||||
<div style="height: 100px"></div>
|
||||
</div>
|
||||
<!-- 全局通知详情弹窗 -->
|
||||
<Teleport to="body">
|
||||
<ElDialog v-model="dialogVisible" title="通知详情" align-center :append-to-body="true" width="640px">
|
||||
<div class="detail-dialog">
|
||||
<div class="section">
|
||||
<div class="info-grid">
|
||||
<div class="label">发送人</div>
|
||||
<div class="value">{{ noticeDetail.sender_name || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">通知标题</div>
|
||||
<div class="value">{{ noticeDetail.title || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="label" style="margin-bottom: 8px">通知内容</div>
|
||||
<div class="content-box">
|
||||
<div class="message-text" v-html="noticeDetail.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -97,7 +123,10 @@
|
||||
import avatar6 from '@/assets/img/avatar/avatar6.webp'
|
||||
import AppConfig from '@/config'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import { mittBus } from '@/utils/sys'
|
||||
const dialogVisible = ref(false)
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
@@ -123,85 +152,38 @@
|
||||
{
|
||||
name: computed(() => t('notice.bar[0]')),
|
||||
num: 1
|
||||
},
|
||||
{
|
||||
name: computed(() => t('notice.bar[1]')),
|
||||
num: 1
|
||||
},
|
||||
{
|
||||
name: computed(() => t('notice.bar[2]')),
|
||||
num: 0
|
||||
}
|
||||
])
|
||||
|
||||
const noticeList = [
|
||||
{
|
||||
title: '新增国际化',
|
||||
time: '2024-6-13 0:10',
|
||||
type: 'notice'
|
||||
},
|
||||
{
|
||||
title: '冷月呆呆给你发了一条消息',
|
||||
time: '2024-4-21 8:05',
|
||||
type: 'message'
|
||||
},
|
||||
{
|
||||
title: '小肥猪关注了你',
|
||||
time: '2020-3-17 21:12',
|
||||
type: 'collection'
|
||||
},
|
||||
{
|
||||
title: '新增使用文档',
|
||||
time: '2024-02-14 0:20',
|
||||
type: 'notice'
|
||||
},
|
||||
{
|
||||
title: '小肥猪给你发了一封邮件',
|
||||
time: '2024-1-20 0:15',
|
||||
type: 'email'
|
||||
},
|
||||
{
|
||||
title: '菜单mock本地真实数据',
|
||||
time: '2024-1-17 22:06',
|
||||
type: 'notice'
|
||||
}
|
||||
]
|
||||
const msgList: any = [
|
||||
{
|
||||
title: '池不胖 关注了你',
|
||||
time: '2021-2-26 23:50',
|
||||
avatar: avatar1
|
||||
},
|
||||
{
|
||||
title: '唐不苦 关注了你',
|
||||
time: '2021-2-21 8:05',
|
||||
avatar: avatar2
|
||||
},
|
||||
{
|
||||
title: '中小鱼 关注了你',
|
||||
time: '2020-1-17 21:12',
|
||||
avatar: avatar3
|
||||
},
|
||||
{
|
||||
title: '何小荷 关注了你',
|
||||
time: '2021-01-14 0:20',
|
||||
avatar: avatar4
|
||||
},
|
||||
{
|
||||
title: '誶誶淰 关注了你',
|
||||
time: '2020-12-20 0:15',
|
||||
avatar: avatar5
|
||||
},
|
||||
{
|
||||
title: '冷月呆呆 关注了你',
|
||||
time: '2020-12-17 22:06',
|
||||
avatar: avatar6
|
||||
}
|
||||
]
|
||||
|
||||
const changeBar = (index: number) => {
|
||||
barActiveIndex.value = index
|
||||
const noticeList = ref<any>([])
|
||||
const getNoticeList = async () => {
|
||||
await commonService.getUnReadNoticeList().then((res: any) => {
|
||||
noticeList.value = res.list
|
||||
// 通知头部组件更新铃铛状态
|
||||
mittBus.emit('notification-data-changed', res.list?.length || 0)
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
getNoticeList()
|
||||
// 监听WebSocket消息:收到新通知实时插入到列表首位
|
||||
mittBus.on('ws:message', onWsMessage)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
mittBus.off('ws:message')
|
||||
})
|
||||
|
||||
const onWsMessage = (data: any) => {
|
||||
if (data && data.event === 'notify.receive') {
|
||||
// 将新通知插入到列表首位。后端数据结构需包含 type 与 message 字段
|
||||
// 若结构不同,可在此处做一次字段映射
|
||||
console.log(data)
|
||||
noticeList.value.unshift(data.data.notice_data)
|
||||
console.log(noticeList.value)
|
||||
mittBus.emit('notification-data-changed', noticeList.value.length)
|
||||
}
|
||||
}
|
||||
// const changeBar = (index: number) => {
|
||||
// barActiveIndex.value = index
|
||||
// }
|
||||
|
||||
const getRandomColor = () => {
|
||||
const index = Math.floor(Math.random() * AppConfig.systemMainColor.length)
|
||||
@@ -209,27 +191,27 @@
|
||||
}
|
||||
|
||||
const noticeStyleMap = {
|
||||
email: {
|
||||
4: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-warning))',
|
||||
backgroundColor: 'rgb(var(--art-bg-warning))'
|
||||
},
|
||||
message: {
|
||||
2: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-success))',
|
||||
backgroundColor: 'rgb(var(--art-bg-success))'
|
||||
},
|
||||
collection: {
|
||||
5: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-danger))',
|
||||
backgroundColor: 'rgb(var(--art-bg-danger))'
|
||||
},
|
||||
user: {
|
||||
3: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-info))',
|
||||
backgroundColor: 'rgb(var(--art-bg-info))'
|
||||
},
|
||||
notice: {
|
||||
1: {
|
||||
icon: '',
|
||||
iconColor: 'rgb(var(--art-primary))',
|
||||
backgroundColor: 'rgb(var(--art-bg-primary))'
|
||||
@@ -243,7 +225,11 @@
|
||||
backgroundColor: getRandomColor()
|
||||
}
|
||||
|
||||
const style = noticeStyleMap[type as keyof typeof noticeStyleMap] || defaultStyle
|
||||
// 先转成 unknown,再转成 keyof 类型,避免 TS 报错
|
||||
const key = type as unknown as keyof typeof noticeStyleMap
|
||||
const style = noticeStyleMap[key] || defaultStyle
|
||||
|
||||
// const style = noticeStyleMap[type as keyof typeof noticeStyleMap] || defaultStyle
|
||||
|
||||
return {
|
||||
...style,
|
||||
@@ -264,27 +250,78 @@
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
|
||||
const readAll = async () => {
|
||||
const { message } = await commonService.readNotice({
|
||||
msg_ids: 'all'
|
||||
})
|
||||
ElMessage.success(message)
|
||||
getNoticeList()
|
||||
}
|
||||
// 查看全部
|
||||
const handleViewAll = () => {
|
||||
switch (barActiveIndex.value) {
|
||||
case 0:
|
||||
handleNoticeAll()
|
||||
break
|
||||
case 1:
|
||||
handleMsgAll()
|
||||
break
|
||||
case 2:
|
||||
handlePendingAll()
|
||||
break
|
||||
}
|
||||
handleNoticeAll()
|
||||
}
|
||||
const noticeDetail = ref<any>({})
|
||||
const showfn = async (row: any) => {
|
||||
dialogVisible.value = true
|
||||
await commonService.getNoticeDetail({ id: row.id }).then((res: any) => {
|
||||
noticeDetail.value = res.message
|
||||
showNotice(false) // 关闭通知组件
|
||||
getNoticeList()
|
||||
})
|
||||
}
|
||||
const handleNoticeAll = () => {
|
||||
router.push('/common/notice')
|
||||
showNotice(false) // 关闭通知组件
|
||||
}
|
||||
|
||||
const handleNoticeAll = () => {}
|
||||
const handleMsgAll = () => {}
|
||||
const handlePendingAll = () => {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@use './style';
|
||||
.el-button {
|
||||
--el-button-border-color: #dcdfe6 !important;
|
||||
--el-button-text-color: var(--el-text-color-regular);
|
||||
}
|
||||
.message-text {
|
||||
padding: 10px 14px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
}
|
||||
.detail-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.section {
|
||||
background: var(--el-fill-color-blank);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
gap: 6px 12px;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
.content-box {
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
<RouterView
|
||||
v-if="isRefresh"
|
||||
:key="$route.path"
|
||||
v-slot="{ Component, route }"
|
||||
:style="{ minHeight: containerMinHeight }"
|
||||
>
|
||||
@@ -15,8 +16,9 @@
|
||||
|
||||
<!-- 路由动画 -->
|
||||
<Transition :name="pageTransition" mode="out-in" appear>
|
||||
<!-- <KeepAlive :max="10" :include="['StockingAdd', 'StockingFinalAdd']"> -->
|
||||
<KeepAlive :max="10" :exclude="keepAliveExclude">
|
||||
<component :is="Component" :key="route.path" v-if="route.meta.keepAlive" />
|
||||
<component :is="Component" v-if="route.meta.keepAlive" />
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
|
||||
@@ -32,10 +34,11 @@
|
||||
import { useWorktabStore } from '@/store/modules/worktab'
|
||||
import { useSettingStore } from '@/store/modules/setting'
|
||||
|
||||
// Store refs
|
||||
// Store refs1
|
||||
const { pageTransition, containerWidth, refresh } = storeToRefs(useSettingStore())
|
||||
const { keepAliveExclude } = storeToRefs(useWorktabStore())
|
||||
|
||||
|
||||
const { containerMinHeight } = useCommon()
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
<div v-if="!isLock">
|
||||
<el-dialog v-model="visible" :width="370" :show-close="false" @open="handleDialogOpen">
|
||||
<div class="lock-content">
|
||||
<img class="cover" src="@imgs/user/avatar.webp" />
|
||||
<img
|
||||
class="cover"
|
||||
:src="getUserInfo.user.avatar ? getUserInfo.user.avatar : '@imgs/user/avatar.webp'"
|
||||
/>
|
||||
<div class="username">{{ userInfo.userName }}</div>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" @submit.prevent="handleLock">
|
||||
<el-form-item prop="password">
|
||||
@@ -32,7 +35,10 @@
|
||||
|
||||
<div class="unlock-content" v-else>
|
||||
<div class="box">
|
||||
<img class="cover" src="@imgs/user/avatar.webp" />
|
||||
<img
|
||||
class="cover"
|
||||
:src="getUserInfo.user.avatar ? getUserInfo.user.avatar : '@imgs/user/avatar.webp'"
|
||||
/>
|
||||
<div class="username">{{ userInfo.userName }}</div>
|
||||
<el-form
|
||||
ref="unlockFormRef"
|
||||
@@ -77,7 +83,7 @@
|
||||
import { mittBus } from '@/utils/sys'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { getUserInfo } = useUserStore()
|
||||
const ENCRYPT_KEY = import.meta.env.VITE_LOCK_ENCRYPT_KEY
|
||||
const userStore = useUserStore()
|
||||
const { info: userInfo, lockPassword, isLock } = storeToRefs(userStore)
|
||||
|
||||
@@ -184,12 +184,11 @@
|
||||
const elTable = tableRef.value
|
||||
if (!elTable) return
|
||||
|
||||
// 递归处理树形数据
|
||||
// 支持普通与树形数据行展开
|
||||
const processRows = (rows: any[]) => {
|
||||
rows.forEach((row) => {
|
||||
const hasChildren = row.children?.length > 0
|
||||
if (hasChildren) {
|
||||
elTable.toggleRowExpansion(row, true)
|
||||
elTable.toggleRowExpansion(row, true)
|
||||
if (row.children?.length > 0) {
|
||||
processRows(row.children)
|
||||
}
|
||||
})
|
||||
@@ -203,12 +202,11 @@
|
||||
const elTable = tableRef.value
|
||||
if (!elTable) return
|
||||
|
||||
// 递归处理树形数据
|
||||
// 支持普通与树形数据行收起
|
||||
const processRows = (rows: any[]) => {
|
||||
rows.forEach((row) => {
|
||||
const hasChildren = row.children?.length > 0
|
||||
if (hasChildren) {
|
||||
elTable.toggleRowExpansion(row, false)
|
||||
elTable.toggleRowExpansion(row, false)
|
||||
if (row.children?.length > 0) {
|
||||
processRows(row.children)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<slot name="left"></slot>
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="right"></slot>
|
||||
<div class="btn" @click="refresh">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
@@ -68,7 +69,6 @@
|
||||
}}</ElCheckbox>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HOME_PAGE } from '@/router/routesAlias'
|
||||
// import { HOME_PAGE } from '@/router/routesAlias'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
const { home_page } = useUserStore()
|
||||
const router = useRouter()
|
||||
|
||||
interface ExceptionData {
|
||||
/** 标题 */
|
||||
title: string
|
||||
@@ -33,7 +34,7 @@
|
||||
)
|
||||
|
||||
const backHome = () => {
|
||||
router.push(HOME_PAGE)
|
||||
router.push(home_page)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="approval-timeline">
|
||||
<div v-for="(rec, idx) in records" :key="idx" class="timeline-item">
|
||||
<div class="left">
|
||||
<div class="avatar-box">
|
||||
<span class="initial">{{ rec.name?.slice(0, 1) || '—' }}</span>
|
||||
<i v-if="rec.approved" class="status-dot iconfont-sys"></i>
|
||||
<i v-if="rec.rejected" class="status-dot rejected"></i>
|
||||
<i v-else-if="rec.pending" class="status-dot pending iconfont-sys"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="row name-role">
|
||||
<span class="name">{{ rec.name }}</span>
|
||||
<span class="role">({{ rec.role }})</span>
|
||||
</div>
|
||||
<div >
|
||||
审核说明:{{ rec.comment }}
|
||||
</div>
|
||||
<div class="row time">{{ rec.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!records || !records.length" class="empty">暂无审批记录</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface ApprovalRecordItem {
|
||||
name?: string
|
||||
role?: string
|
||||
type?: string
|
||||
time?: string
|
||||
approved?: boolean
|
||||
rejected?: boolean
|
||||
pending?: boolean
|
||||
comment?: string
|
||||
}
|
||||
|
||||
defineProps<{ records: ApprovalRecordItem[] }>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 审批记录 时间线 */
|
||||
.approval-timeline {
|
||||
padding: 8px 4px 8px 0;
|
||||
}
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.timeline-item + .timeline-item {
|
||||
margin-top: 50px;
|
||||
}
|
||||
.left {
|
||||
width: 46px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
background: #3d7bff;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
}
|
||||
.status-dot {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
font-size: 9px;
|
||||
padding: 3px 1px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.status-dot.pending {
|
||||
background: #3471ff;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
.status-dot.rejected {
|
||||
background: #ff0000;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
/* 连接下一个节点的虚线 */
|
||||
.timeline-item:not(:last-child) .left::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 52px; /* avatar 46px + 间距 6px */
|
||||
left: 23px; /* 盒子宽度一半,确保竖线居中 */
|
||||
width: 0;
|
||||
height: calc(100% + 16px); /* 加上相邻项之间的间距 */
|
||||
border-left: 2px dotted #bfbfbf;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.row.name-role {
|
||||
color: #222;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.role {
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.time {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.empty {
|
||||
color: #999;
|
||||
padding: 8px 0 4px 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,583 @@
|
||||
<template>
|
||||
<div class="order-detail-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="basic-info-section">
|
||||
<div class="basic-info-item">
|
||||
<span class="label">备货单编号:</span>
|
||||
<span class="value">{{ orderData.order_no || orderData.orderNo || '—' }}</span>
|
||||
</div>
|
||||
<div class="basic-info-item">
|
||||
<span class="label">业务员:</span>
|
||||
<span class="value">{{
|
||||
orderData.salesperson_snapshot || orderData.salesperson || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客户信息 -->
|
||||
<div class="customer-info-table">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<td>
|
||||
<span class="info-label"> 阿里订单号/PI号:</span>
|
||||
<span class="info-value">{{ orderData.pi_no || orderData.piNo || '—' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 日期:</span>
|
||||
<span class="info-value">{{ orderData.created_at || orderData.date || '—' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 清关目的国:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.clearance_country_snapshot || orderData.clearanceCountry || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 客户来源:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.customer_source_snapshot || orderData.customerSource || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 客户名称:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.customer_snapshot || orderData.customerName || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 客户所在国:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.customer_country_snapshot || orderData.customerCountry || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情表格(ArtTable) -->
|
||||
<div class="table-section">
|
||||
<ArtTable ref="tableRef" :data="vehItems" border :pagination="false">
|
||||
<template #default>
|
||||
<ElTableColumn type="expand" width="30">
|
||||
<template #default="scope">
|
||||
<div style="padding: 6px">
|
||||
<strong>备注:</strong>
|
||||
<span>{{ scope.row.remark || '暂无备注' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="编号" width="60" align="center">
|
||||
<template #default="scope">{{ scope.$index + 1 }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="车型" min-width="160">
|
||||
<template #default="scope">
|
||||
{{ scope.row.model_snapshot || scope.row.model || '—' }}
|
||||
<span v-if="scope.row.mileage > 0">({{ scope.row.mileage }}km)</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="数量/台" width="90" align="center">
|
||||
<template #default="scope">{{ scope.row.num ?? scope.row.quantity ?? 1 }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="颜色/外" align="center" prop="color_exterior_snapshot" />
|
||||
<ElTableColumn
|
||||
label="内饰"
|
||||
min-width="120"
|
||||
align="center"
|
||||
prop="color_interior_snapshot"
|
||||
/>
|
||||
<ElTableColumn label="改装" min-width="120" align="center" prop="modify" />
|
||||
<ElTableColumn label="官方指导价/二手挂牌价" min-width="175" align="center">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.mileage > 0">
|
||||
{{
|
||||
scope.row.listing_price?.currency_info ||
|
||||
scope.row.listing_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{
|
||||
scope.row.official_price?.currency_info ||
|
||||
scope.row.official_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="优惠" min-width="120" align="center">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.mileage > 0">
|
||||
{{
|
||||
scope.row.reduction_price?.currency_info ||
|
||||
scope.row.reduction_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{
|
||||
scope.row.discount_price?.currency_info ||
|
||||
scope.row.discount_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}
|
||||
</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="手续费" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.handling_fee?.currency_info ||
|
||||
scope.row.handling_fee?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="改装费" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.retrofit_fee?.currency_info ||
|
||||
scope.row.retrofit_fee?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="FOB运费" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.fob_transport_fee?.currency_info ||
|
||||
scope.row.fob_transport_fee?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="港杂费" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.port_misc_fee?.currency_info ||
|
||||
scope.row.port_misc_fee?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="海运费" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.ocean_freight?.currency_info ||
|
||||
scope.row.ocean_freight?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="佣金" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.commission?.currency_info ||
|
||||
scope.row.commission?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="购置税" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.purchase_tax?.currency_info ||
|
||||
scope.row.purchase_tax?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="预估退税额" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.tax_refund?.currency_info ||
|
||||
scope.row.tax_refund?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="单价" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.unit_price?.currency_info ||
|
||||
scope.row.unit_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="总价" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.total_price?.currency_info ||
|
||||
scope.row.total_price?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="利润" min-width="120" align="center">
|
||||
<template #default="scope">{{
|
||||
scope.row.profit?.currency_info ||
|
||||
scope.row.profit?.amount ||
|
||||
scope.row.discount ||
|
||||
'—'
|
||||
}}</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
<div class="order-summary">
|
||||
<div class="summary-item">
|
||||
<span class="label">合同汇率:</span>
|
||||
<span class="value">{{
|
||||
orderData.contract_exchange_rate || orderData.contractRate || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">合计到岸价:</span>
|
||||
<span class="value"
|
||||
>${{
|
||||
formatPrice(Number(orderData.cif_total_amount?.amount ?? orderData.totalCif ?? 0))
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="label">利润:</span>
|
||||
<span class="value">{{
|
||||
orderData.profit_total?.currency_info || orderData.profit_total?.amount || '—'
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 物流信息 -->
|
||||
<div class="customer-info-table">
|
||||
<table class="info-table">
|
||||
<tr>
|
||||
<td>
|
||||
<span class="info-label"> 清关人:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.declarant_name || orderData.declarant || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 预计发运日期:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.eta_ship_date || orderData.etaShipDate || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 出运方式:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.shipping_method_snapshot || orderData.shippingMethod || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 国内收货港:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.domestic_port_snapshot || orderData.domesticPort || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 贸易条款/清关目的港:</span>
|
||||
<span class="info-value"
|
||||
>{{ orderData.trade_term_snapshot || orderData.tradeTerms || '—' }}/{{
|
||||
orderData.clearance_port_snapshot || '—'
|
||||
}}</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<span class="info-label"> 最迟到港时间:</span>
|
||||
<span class="info-value">{{
|
||||
orderData.latest_arrival_date || orderData.latestArrival || '—'
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 计划收款信息 -->
|
||||
<div
|
||||
class="payment-section"
|
||||
v-if="
|
||||
(orderData.account_list && orderData.account_list.length) ||
|
||||
(orderData.payments && orderData.payments.length)
|
||||
"
|
||||
>
|
||||
<div class="table-wrapper">
|
||||
<table class="payment-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 140px"></th>
|
||||
<th>序号</th>
|
||||
<th>汇款国</th>
|
||||
<th>日期</th>
|
||||
<th>币种</th>
|
||||
<th>账户</th>
|
||||
<th>汇款人</th>
|
||||
<th>金额</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row, index) in paymentTableData" :key="index">
|
||||
<td v-if="index === 0" :rowspan="paymentTableData.length">计划收款信息</td>
|
||||
<td>{{ row.serial }}</td>
|
||||
<td>{{ row.country }}</td>
|
||||
<td>{{ row.date }}</td>
|
||||
<td>{{ row.currency }}</td>
|
||||
<td>{{ row.account }}</td>
|
||||
<td>{{ row.remitter }}</td>
|
||||
<td>{{ row.amount }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed, ref, onMounted, nextTick, watch } from 'vue'
|
||||
export default defineComponent({
|
||||
name: 'OrderDetailContent',
|
||||
props: {
|
||||
orderData: {
|
||||
type: Object as () => any,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const paymentTableData = computed(() => {
|
||||
const list = (props.orderData as any).account_list
|
||||
? (props.orderData as any).account_list.map((p: any, idx: number) => ({
|
||||
group: '计划收款信息',
|
||||
serial: `收款${idx + 1}`,
|
||||
country: p.remit_country_name_snapshot || '—',
|
||||
date: p.remit_date || '—',
|
||||
currency: p.currency_name_snapshot || '—',
|
||||
account: p.account_name_snapshot || '—',
|
||||
remitter: p.remitter_name || '—',
|
||||
amount: `$${formatPrice(Number(p.amount || 0))}`
|
||||
}))
|
||||
: (props.orderData as any).payments || []
|
||||
if (!list.length) return []
|
||||
return list
|
||||
})
|
||||
|
||||
const formatPrice = (price?: number): string => {
|
||||
if (price === undefined || price === null) return '0.00'
|
||||
return Number(price).toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
}
|
||||
|
||||
const vehItems = computed(() => {
|
||||
return (props.orderData as any).veh_list || (props.orderData as any).items || []
|
||||
})
|
||||
const tableRef = ref()
|
||||
const expandWhenReady = () => {
|
||||
nextTick(() => {
|
||||
nextTick(() => {
|
||||
if (vehItems.value && vehItems.value.length) {
|
||||
tableRef.value?.expandAll?.()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 默认展开所有行(等数据和表格渲染完成后再触发)
|
||||
expandWhenReady()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => vehItems.value?.length,
|
||||
(len) => {
|
||||
if (len && len > 0) expandWhenReady()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
return {
|
||||
paymentTableData,
|
||||
vehItems,
|
||||
formatPrice,
|
||||
tableRef
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.order-detail-content {
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.basic-info-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.basic-info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.value {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.customer-info-table {
|
||||
margin: 20px 0px;
|
||||
}
|
||||
|
||||
.info-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
td {
|
||||
padding: 12px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
font-size: 13px;
|
||||
}
|
||||
.info-value {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
text-align: right;
|
||||
}
|
||||
.info-label {
|
||||
color: #666;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-section {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.order-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
|
||||
th {
|
||||
background: #f5f7fa;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
padding: 12px 8px;
|
||||
text-align: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 8px;
|
||||
text-align: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
color: #333;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.col-no {
|
||||
width: 60px;
|
||||
}
|
||||
.col-model {
|
||||
max-width: 160px;
|
||||
// text-align: left;
|
||||
}
|
||||
tbody tr:hover {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-section {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* 订单底部汇总样式 */
|
||||
.order-summary {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.order-summary .summary-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
// background: #fff;
|
||||
// border: 1px solid #e5e7eb;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.order-summary .label {
|
||||
color: #666;
|
||||
}
|
||||
.order-summary .value {
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
/* ArtTable 表头背景色 */
|
||||
.table-section :deep(.el-table__header th) {
|
||||
background-color: #f5f7fa;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
.table-section :deep(.el-table__header .cell) {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
.payment-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
.payment-table th {
|
||||
background: #f5f7fa;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
border: 1px solid #d9d9d9;
|
||||
padding: 12px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.payment-table td {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
border: 1px solid #d9d9d9;
|
||||
padding: 10px 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.el-table__expand-cell {
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
}
|
||||
.el-table__expand-wrapper {
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
/* 打印优化 */
|
||||
@media print {
|
||||
.order-detail-content {
|
||||
font-size: 12px;
|
||||
}
|
||||
.order-table {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<div class="detail-wrap">
|
||||
<div class="detail-grid">
|
||||
<div class="item">
|
||||
<span class="label">采购单号:</span>
|
||||
<span class="value">{{ detailData.order_no || '—' }}</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">PI号:</span>
|
||||
<span class="value">{{ detailData.pi_no || '—' }}</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">提交人:</span>
|
||||
<span class="value">{{ detailData.buyer_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="item full">
|
||||
<span class="label">备注:</span>
|
||||
<span class="value">{{ detailData.remark || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-block">
|
||||
<div class="block-title">合同文件</div>
|
||||
<div
|
||||
v-if="detailData.contract_file_url && detailData.contract_file_url.length"
|
||||
class="files-list"
|
||||
>
|
||||
<div class="file-row" v-for="(f, idx) in detailData.contract_file_url" :key="idx">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span class="file-name">{{ f.name }}</span>
|
||||
<a class="el-link el-link--primary" :href="f.url" target="_blank" rel="noopener">查看</a>
|
||||
<a class="el-link el-link--info" :href="f.url" :download="f.name">下载</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">无合同文件</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-block">
|
||||
<div class="block-title">备货单组</div>
|
||||
<div v-if="detailData.stock_order && detailData.stock_order.length" class="groups">
|
||||
<div class="group" v-for="(g, gi) in detailData.stock_order" :key="gi">
|
||||
<div class="group-head">
|
||||
<span class="order">{{ g.order_no || '备货单' }}</span>
|
||||
<span class="seller" v-if="g.salesperson_snapshot"
|
||||
>({{ g.salesperson_snapshot }})</span
|
||||
>
|
||||
<span class="date" v-if="g.biz_date">{{ g.biz_date }}</span>
|
||||
</div>
|
||||
<div class="chips">
|
||||
<span class="chip" v-for="(v, vi) in g.stock_order_items || []" :key="vi">
|
||||
{{ v.model_snapshot }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">无备货单数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import { PropType } from 'vue'
|
||||
|
||||
// 定义组件的数据类型
|
||||
interface DetailData {
|
||||
order_no?: string
|
||||
pi_no?: string
|
||||
buyer_name?: string
|
||||
remark?: string
|
||||
contract_file_url?: Array<{ name: string; url: string }>
|
||||
stock_order?: Array<{
|
||||
order_no?: string
|
||||
salesperson_snapshot?: string
|
||||
biz_date?: string
|
||||
stock_order_items?: Array<{ model_snapshot: string }>
|
||||
}>
|
||||
}
|
||||
|
||||
// 使用 Vue 的 props 传递数据
|
||||
export default {
|
||||
name: 'ProcurementDetailView',
|
||||
props: {
|
||||
detailData: {
|
||||
type: Object as PropType<DetailData>,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px 16px;
|
||||
}
|
||||
.detail-grid .item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.detail-grid .item.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.detail-grid .label {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.detail-grid .value {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.detail-block {
|
||||
border-top: 1px dashed var(--el-border-color-lighter);
|
||||
padding-top: 8px;
|
||||
}
|
||||
.block-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.files-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.file-name {
|
||||
flex: 1;
|
||||
}
|
||||
.empty-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 38vh;
|
||||
overflow-y: auto;
|
||||
gap: 12px;
|
||||
}
|
||||
.group {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
padding: 14px 6px 8px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
.group-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
align-content: center;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.chip {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
background: #f0f6ff;
|
||||
color: #3a7af3;
|
||||
border: 1px solid #e3edff;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="detail-wrap">
|
||||
<div class="detail-grid">
|
||||
<div class="item">
|
||||
<span class="label">采购单号:</span>
|
||||
<span class="value">{{ detailData.order_no || '—' }}</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">PI号:</span>
|
||||
<span class="value">{{ detailData.pi_no || '—' }}</span>
|
||||
</div>
|
||||
<div class="item">
|
||||
<span class="label">提交人:</span>
|
||||
<span class="value">{{ detailData.buyer_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="item full">
|
||||
<span class="label">备注:</span>
|
||||
<span class="value">{{ detailData.remark || '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-block">
|
||||
<div class="block-title">合同文件</div>
|
||||
<div
|
||||
v-if="detailData.contract_file_url && detailData.contract_file_url.length"
|
||||
class="files-list"
|
||||
>
|
||||
<div class="file-row" v-for="(f, idx) in detailData.contract_file_url" :key="idx">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span class="file-name">{{ f.name }}</span>
|
||||
<a class="el-link el-link--primary" :href="f.url" target="_blank" rel="noopener">查看</a>
|
||||
<a class="el-link el-link--info" :href="f.url" :download="f.name">下载</a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">无合同文件</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-block">
|
||||
<div class="block-title">备货单组</div>
|
||||
<div v-if="detailData.stock_order && detailData.stock_order.length" class="groups">
|
||||
<div class="group" v-for="(g, gi) in detailData.stock_order" :key="gi">
|
||||
<div class="group-head">
|
||||
<span class="order">{{ g.order_no || '备货单' }}</span>
|
||||
<span class="seller" v-if="g.salesperson_snapshot"
|
||||
>({{ g.salesperson_snapshot }})</span
|
||||
>
|
||||
<span class="date" v-if="g.biz_date">{{ g.biz_date }}</span>
|
||||
</div>
|
||||
<div class="chips">
|
||||
<span class="chip" v-for="(v, vi) in g.stock_order_items || []" :key="vi">
|
||||
{{ v.model_snapshot }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-tip">无备货单数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Document } from '@element-plus/icons-vue'
|
||||
import { PropType } from 'vue'
|
||||
interface DetailData {
|
||||
order_no?: string
|
||||
pi_no?: string
|
||||
buyer_name?: string
|
||||
remark?: string
|
||||
contract_file_url?: Array<{ name: string; url: string }>
|
||||
stock_order?: Array<{
|
||||
order_no?: string
|
||||
salesperson_snapshot?: string
|
||||
biz_date?: string
|
||||
stock_order_items?: Array<{ model_snapshot: string }>
|
||||
}>
|
||||
}
|
||||
|
||||
defineProps({
|
||||
detailData: {
|
||||
type: Object as PropType<DetailData>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px 16px;
|
||||
}
|
||||
.detail-grid .item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.detail-grid .item.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.detail-grid .label {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
.detail-grid .value {
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.detail-block {
|
||||
border-top: 1px dashed var(--el-border-color-lighter);
|
||||
padding-top: 8px;
|
||||
}
|
||||
.block-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.files-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.file-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.file-name {
|
||||
flex: 1;
|
||||
}
|
||||
.empty-tip {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 38vh;
|
||||
overflow-y: auto;
|
||||
gap: 12px;
|
||||
}
|
||||
.group {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
padding: 14px 6px 8px;
|
||||
background: var(--el-fill-color-blank);
|
||||
}
|
||||
.group-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
align-content: center;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.chip {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
background: #f0f6ff;
|
||||
color: #3a7af3;
|
||||
border: 1px solid #e3edff;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<el-drawer :with-header="false" :model-value="modelValue" :size="isMobile ? '100%' : '480px'" @close="$emit('update:modelValue', false)">
|
||||
<div class="chat-container">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<span class="name">{{ title }}</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-icon class="icon-close" :size="20" @click="$emit('update:modelValue', false)">
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages" ref="messageContainer">
|
||||
<template v-for="(message, index) in messages" :key="index">
|
||||
<div :class="['message-item', String(message.user_id) === String(currentUserId) ? 'message-right' : 'message-left']">
|
||||
<el-avatar :size="32" :src="message.user_avatar" class="message-avatar" />
|
||||
<div class="message-content">
|
||||
<div class="message-info">
|
||||
<span class="sender-name">{{ message.user_name }}</span>
|
||||
<span class="message-time">{{ message.updated_at }}</span>
|
||||
</div>
|
||||
<div class="message-text" v-html="message.content"></div>
|
||||
<div class="message-attachments" v-if="message.attachment && message.attachment.length > 0">
|
||||
<div class="attachment-item" v-for="(attachment, attIndex) in message.attachment" :key="attIndex">
|
||||
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(attachment.original_name)">
|
||||
<img :src="attachment.storage_path_full" :alt="attachment.original_name" class="attachment-thumb" @click="handleChatImageClick(attachment.storage_path_full)" />
|
||||
</template>
|
||||
<template v-else-if="/\.(mp4|webm|ogg)$/i.test(attachment.original_name)">
|
||||
<video :src="attachment.storage_path_full" controls class="attachment-thumb" @click.prevent="handleChatVideoClick(attachment.storage_path_full)"></video>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="file-attachment">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span class="file-name">{{ attachment.original_name }}</span>
|
||||
<el-button type="primary" size="small" link @click="$emit('download', attachment)">下载</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="chat-input" v-if="canAdd">
|
||||
<div class="chat-attachment-upload">
|
||||
<el-upload v-model:file-list="chatFileList" class="chat-upload" :action="actionImageUplode" :on-success="handleChatUploadSuccess" :headers="uploadHeaders" multiple :limit="5" :on-exceed="handleChatExceed" :show-file-list="false">
|
||||
<el-button type="primary" link>
|
||||
<el-icon><Paperclip /></el-icon>
|
||||
添加附件
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<div class="uploaded-attachments" v-if="chatFileList.length > 0">
|
||||
<div class="attachment-preview-item" v-for="(file, idx) in chatFileList" :key="idx">
|
||||
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(file.name)">
|
||||
<img :src="file.file_url || file.response?.data?.file_url" :alt="file.name" class="preview-thumb" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
</template>
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<el-button type="danger" size="small" link @click="removeChatFile(idx)">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-input v-model="messageText" type="textarea" :rows="3" placeholder="输入消息" resize="none" />
|
||||
<div class="chat-input-actions">
|
||||
<el-button type="primary" @click="emitSend">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内置预览 -->
|
||||
<ElImageViewer v-if="imageViewerVisible" :url-list="chatImageList" :initial-index="imageViewerIndex" @close="closeImageViewer" />
|
||||
<ElDialog v-model="videoDialogVisible" width="60%" top="10vh" :show-close="true" append-to-body>
|
||||
<video v-if="currentVideoUrl" :src="currentVideoUrl" controls autoplay style="width: 100%; max-height: 70vh; display: block"></video>
|
||||
</ElDialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElImageViewer } from 'element-plus'
|
||||
import { Paperclip, Document, Close } from '@element-plus/icons-vue'
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
title?: string
|
||||
messages: any[]
|
||||
currentUserId: string | number
|
||||
canAdd?: boolean
|
||||
actionImageUplode?: string
|
||||
uploadHeaders?: Record<string, any>
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: boolean): void
|
||||
(e: 'send', payload: { text: string; attachments: any[] }): void
|
||||
(e: 'download', attachment: any): void
|
||||
}>()
|
||||
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = computed(() => width.value < 500)
|
||||
|
||||
const messageText = ref('')
|
||||
const chatFileList = ref<any[]>([])
|
||||
const messageContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
watch(
|
||||
() => props.messages,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
if (messageContainer.value) {
|
||||
messageContainer.value.scrollTop = messageContainer.value.scrollHeight
|
||||
}
|
||||
})
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
const handleChatUploadSuccess = (response: any, uploadFile: any, uploadFiles: any[]) => {
|
||||
chatFileList.value = uploadFiles
|
||||
.filter((item) => item.status === 'success')
|
||||
.map((item) => {
|
||||
const data = item.response?.data
|
||||
if (data) {
|
||||
return {
|
||||
name: data.original_name,
|
||||
file_url: data.storage_path_full,
|
||||
status: 'success',
|
||||
response: { data }
|
||||
}
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter(Boolean) as any[]
|
||||
}
|
||||
const handleChatExceed = () => {
|
||||
// no-op; parent can handle toast
|
||||
}
|
||||
const removeChatFile = (index: number) => {
|
||||
chatFileList.value.splice(index, 1)
|
||||
}
|
||||
const emitSend = () => {
|
||||
const text = messageText.value.trim()
|
||||
if (!text && chatFileList.value.length === 0) return
|
||||
const attachments = chatFileList.value.map((file) => {
|
||||
const data = file.response?.data
|
||||
if (data) {
|
||||
return {
|
||||
original_name: data.original_name,
|
||||
storage_path: data.storage_path || data.file_name
|
||||
}
|
||||
}
|
||||
return null
|
||||
}).filter(Boolean) as any[]
|
||||
emit('send', { text, attachments })
|
||||
messageText.value = ''
|
||||
chatFileList.value = []
|
||||
}
|
||||
|
||||
// 预览
|
||||
const chatImageList = ref<string[]>([])
|
||||
const imageViewerVisible = ref(false)
|
||||
const imageViewerIndex = ref(0)
|
||||
function handleChatImageClick(url: string) {
|
||||
chatImageList.value = [url]
|
||||
imageViewerIndex.value = 0
|
||||
imageViewerVisible.value = true
|
||||
}
|
||||
const currentVideoUrl = ref('')
|
||||
const videoDialogVisible = ref(false)
|
||||
function handleChatVideoClick(url: string) {
|
||||
currentVideoUrl.value = url
|
||||
videoDialogVisible.value = true
|
||||
}
|
||||
function closeImageViewer() {
|
||||
imageViewerVisible.value = false
|
||||
chatImageList.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
.header-left .name { font-size: 16px; font-weight: 500; }
|
||||
.header-right .icon-close { cursor: pointer; }
|
||||
}
|
||||
.chat-container { display: flex; flex-direction: column; height: calc(100% - 70px); }
|
||||
.chat-messages { flex: 1; padding: 30px 16px; overflow-y: auto; border-top: 1px solid var(--el-border-color-lighter); }
|
||||
.message-item { display: flex; gap: 8px; align-items: flex-start; width: 100%; margin-bottom: 30px; }
|
||||
.message-item.message-left { justify-content: flex-start; }
|
||||
.message-item.message-right { flex-direction: row-reverse; }
|
||||
.message-content { display: flex; flex-direction: column; max-width: 70%; }
|
||||
.message-info { display: flex; gap: 8px; margin-bottom: 4px; font-size: 12px; }
|
||||
.message-text { font-size: 14px; color: var(--art-gray-900); border-radius: 6px; padding: 10px 14px; line-height: 1.4; white-space: pre-line; }
|
||||
.message-left .message-text { background-color: #f8f5ff; }
|
||||
.message-right .message-text { background-color: #e9f3ff; }
|
||||
.message-attachments { margin-top: 8px; }
|
||||
.message-attachments .attachment-thumb { max-width: 200px; max-height: 150px; border-radius: 6px; cursor: pointer; border: 1px solid var(--el-border-color-lighter); }
|
||||
.chat-input { padding: 16px 16px 0; }
|
||||
.chat-input-actions { display: flex; justify-content: right; margin-top: 12px; }
|
||||
.chat-attachment-upload { margin-bottom: 12px; }
|
||||
.uploaded-attachments { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.attachment-preview-item { display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: var(--el-fill-color-light); border-radius: 6px; border: 1px solid var(--el-border-color-lighter); max-width: 200px; }
|
||||
.preview-thumb { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
export interface OrderItem {
|
||||
model?: string
|
||||
quantity?: number
|
||||
exteriorColor?: string
|
||||
interiorColor?: string
|
||||
modification?: string
|
||||
unitPrice?: number
|
||||
totalPrice?: number
|
||||
officialPrice?: number
|
||||
discount?: string
|
||||
}
|
||||
|
||||
export interface PaymentItem {
|
||||
country?: string
|
||||
date?: string
|
||||
currency?: string
|
||||
account?: string
|
||||
remitter?: string
|
||||
amount?: number
|
||||
}
|
||||
|
||||
export interface OrderData {
|
||||
contract_file_url: { name: string; url: string }[]
|
||||
orderNo?: string
|
||||
salesperson?: string
|
||||
piNo?: string
|
||||
date?: string
|
||||
clearanceCountry?: string
|
||||
customerSource?: string
|
||||
customerName?: string
|
||||
customerCountry?: string
|
||||
items?: OrderItem[]
|
||||
contractRate?: string
|
||||
totalCif?: number
|
||||
declarant?: string
|
||||
etaShipDate?: string
|
||||
shippingMethod?: string
|
||||
domesticPort?: string
|
||||
tradeTerms?: string
|
||||
latestArrival?: string
|
||||
payments?: PaymentItem[]
|
||||
}
|
||||
|
||||
export function useOrderDetailDialog() {
|
||||
const visible = ref(false)
|
||||
const title = ref('订单详情')
|
||||
const orderData = ref<OrderData>({
|
||||
contract_file_url: [],
|
||||
items: [],
|
||||
payments: []
|
||||
})
|
||||
|
||||
const show = (data: OrderData, dialogTitle?: string) => {
|
||||
orderData.value = data
|
||||
title.value = dialogTitle || '订单详情'
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const print = () => {
|
||||
// 可以在这里添加打印逻辑
|
||||
window.print()
|
||||
}
|
||||
|
||||
return {
|
||||
visible,
|
||||
title,
|
||||
orderData,
|
||||
show,
|
||||
hide,
|
||||
print
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
export const usePdfPreview = () => {
|
||||
const state = reactive({
|
||||
visible: false,
|
||||
url: '',
|
||||
name: '',
|
||||
blobUrl: '',
|
||||
images: [] as string[]
|
||||
})
|
||||
const frameRef = ref<HTMLIFrameElement | null>(null)
|
||||
|
||||
// 动态加载 pdf.js(优先本地依赖,失败回退 CDN)
|
||||
const loadPdfJs = async (): Promise<any> => {
|
||||
// 统一使用 CDN,避免本地依赖缺失导致打包时报错
|
||||
const ensure = (src: string) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const s = document.createElement('script')
|
||||
s.src = src
|
||||
s.onload = () => resolve()
|
||||
s.onerror = () => reject(new Error('load script failed'))
|
||||
document.head.appendChild(s)
|
||||
})
|
||||
await ensure('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js')
|
||||
await ensure('https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js')
|
||||
return (window as any).pdfjsLib
|
||||
}
|
||||
|
||||
const open = (url: string, name = '') => {
|
||||
if (!url) return
|
||||
state.url = url
|
||||
state.name = name
|
||||
state.visible = true
|
||||
state.images = []
|
||||
}
|
||||
|
||||
const openWithResponse = (res: any) => {
|
||||
const url = res?.data?.file_url_full || res?.file_url_full
|
||||
const name = res?.data?.file_url || res?.file_url || ''
|
||||
open(url, name)
|
||||
}
|
||||
|
||||
const inferFilenameFromUrl = (url: string): string => {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
const parts = u.pathname.split('/')
|
||||
return parts[parts.length - 1] || 'document.pdf'
|
||||
} catch (e) {
|
||||
const parts = (url || '').split('?')[0].split('/')
|
||||
return parts[parts.length - 1] || 'document.pdf'
|
||||
}
|
||||
}
|
||||
|
||||
const download = async () => {
|
||||
if (!state.url) return
|
||||
const filename = state.name || inferFilenameFromUrl(state.url)
|
||||
try {
|
||||
const resp = await fetch(state.url, { credentials: 'omit' })
|
||||
const blob = await resp.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch {
|
||||
window.open(state.url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// 预览时也用 blob,提升兼容性
|
||||
const preparePreview = async () => {
|
||||
if (!state.url) return
|
||||
try {
|
||||
const resp = await fetch(state.url, { credentials: 'omit' })
|
||||
const blob = await resp.blob()
|
||||
state.blobUrl && window.URL.revokeObjectURL(state.blobUrl)
|
||||
state.blobUrl = window.URL.createObjectURL(blob)
|
||||
} catch {
|
||||
state.blobUrl = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 生成图片以便无需浏览器 PDF 插件即可展示
|
||||
const prepareImages = async () => {
|
||||
if (!state.url) return
|
||||
state.images = []
|
||||
try {
|
||||
const resp = await fetch(state.url, { credentials: 'omit' })
|
||||
const arrayBuffer = await resp.arrayBuffer()
|
||||
const pdfjsLib = await loadPdfJs()
|
||||
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise
|
||||
const images: string[] = []
|
||||
for (let p = 1; p <= pdf.numPages; p++) {
|
||||
const page = await pdf.getPage(p)
|
||||
const viewport = page.getViewport({ scale: 1.5 })
|
||||
const canvas = document.createElement('canvas')
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) continue
|
||||
canvas.width = viewport.width
|
||||
canvas.height = viewport.height
|
||||
await page.render({ canvasContext: context, viewport }).promise
|
||||
images.push(canvas.toDataURL('image/png'))
|
||||
}
|
||||
state.images = images
|
||||
} catch {
|
||||
state.images = []
|
||||
}
|
||||
}
|
||||
|
||||
const print = () => {
|
||||
const frame = frameRef.value
|
||||
if (frame && frame.contentWindow) {
|
||||
frame.contentWindow.focus()
|
||||
frame.contentWindow.print()
|
||||
return
|
||||
}
|
||||
if (state.url) {
|
||||
const win = window.open(state.url, '_blank')
|
||||
if (win) {
|
||||
win.addEventListener('load', () => win.print())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pdf: state,
|
||||
frameRef,
|
||||
open,
|
||||
openWithResponse,
|
||||
download,
|
||||
print,
|
||||
preparePreview,
|
||||
prepareImages
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useUserStore } from '@/store/modules/user'
|
||||
export function useUploadHeaders() {
|
||||
const userStore = useUserStore()
|
||||
const uploadHeaders = computed(() => ({
|
||||
'Fdesk-Language': userStore.language,
|
||||
'Fdesk-Corp-Abbr': userStore.companyabbr,
|
||||
'Fdesk-System-Code': 'fdesk',
|
||||
'Fdesk-Login-Type': userStore.loginType,
|
||||
'Fdesk-Language': userStore.language || 'zh',
|
||||
'Fdesk-Corp-Abbr': userStore.companyabbr || 'default',
|
||||
'Fdesk-System-Code': userStore.systemsabbr || 'fdesk',
|
||||
'Fdesk-Login-Type': userStore.loginType || 'upwd',
|
||||
'Fdesk-Auth-Token': 'Bearer ' + userStore.accessToken
|
||||
}))
|
||||
return { uploadHeaders }
|
||||
|
||||
@@ -127,7 +127,7 @@ const appConfig: SystemConfig = {
|
||||
] as const,
|
||||
// 系统其他项默认配置
|
||||
systemSetting: {
|
||||
defaultMenuWidth: 240, // 菜单宽度
|
||||
defaultMenuWidth: 200, // 菜单宽度
|
||||
defaultCustomRadius: '0.75', // 自定义圆角
|
||||
defaultTabStyle: 'tab-default' // 标签样式
|
||||
}
|
||||
|
||||
@@ -59,27 +59,65 @@ async function handleRouteGuard(
|
||||
// 设置系统主题
|
||||
setSystemTheme(to)
|
||||
|
||||
// 处理登录状态
|
||||
if (!(await handleLoginStatus(to, userStore, next))) {
|
||||
return
|
||||
// 特殊处理电子签名页面 - 优先处理,避免登录检查
|
||||
if (to.path === '/auth/signature/mobile') {
|
||||
// 设置认证信息
|
||||
if (to.query.accessToken || to.query.token) {
|
||||
const token = (to.query.accessToken as string) || (to.query.token as string)
|
||||
if (token) {
|
||||
userStore.setToken(token)
|
||||
userStore.setLoginStatus(true)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置其他必要的参数到userStore
|
||||
if (to.query.companyid && to.query.companyabbr) {
|
||||
userStore.setCompany(to.query.companyid as string, to.query.companyabbr as string)
|
||||
}
|
||||
|
||||
if (to.query.systemsid && to.query.systemsabbr) {
|
||||
userStore.setSystems(to.query.systemsid as string, to.query.systemsabbr as string)
|
||||
}
|
||||
|
||||
if (to.query.loginType) {
|
||||
userStore.setLoginType(to.query.loginType as 'feishu' | 'upwd')
|
||||
}
|
||||
|
||||
if (to.matched.length > 0) {
|
||||
setWorktab(to)
|
||||
setPageTitle(to)
|
||||
next()
|
||||
return
|
||||
} else {
|
||||
// 如果路由没有匹配,直接跳转到404
|
||||
next(RoutesAlias.Exception404)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 处理动态路由注册
|
||||
if (!isRouteRegistered.value && userStore.isLogin) {
|
||||
// 处理其他页面的登录状态(跳过电子签名页面)
|
||||
if (to.path !== '/auth/signature/mobile') {
|
||||
if (!(await handleLoginStatus(to, userStore, next))) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 处理动态路由注册(跳过电子签名页面)
|
||||
if (to.path !== '/auth/signature/mobile' && !isRouteRegistered.value && userStore.isLogin) {
|
||||
await handleDynamicRoutes(to, router, next)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理已知的匹配路由
|
||||
if (to.matched.length > 0) {
|
||||
// 处理已知的匹配路由(跳过电子签名页面)
|
||||
if (to.path !== '/auth/signature/mobile' && to.matched.length > 0) {
|
||||
setWorktab(to)
|
||||
setPageTitle(to)
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试刷新路由重新注册
|
||||
if (userStore.isLogin) {
|
||||
// 尝试刷新路由重新注册(跳过电子签名页面)
|
||||
if (to.path !== '/auth/signature/mobile' && userStore.isLogin) {
|
||||
isRouteRegistered.value = false
|
||||
await handleDynamicRoutes(to, router, next)
|
||||
return
|
||||
@@ -97,6 +135,7 @@ async function handleLoginStatus(
|
||||
userStore: ReturnType<typeof useUserStore>,
|
||||
next: NavigationGuardNext
|
||||
): Promise<boolean> {
|
||||
// 检查其他页面的登录状态
|
||||
if (!userStore.isLogin && to.path !== RoutesAlias.Login && !to.meta.noLogin) {
|
||||
userStore.logOut()
|
||||
next({
|
||||
@@ -105,6 +144,7 @@ async function handleLoginStatus(
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -200,22 +240,17 @@ async function registerAndStoreMenu(
|
||||
*/
|
||||
function handleMenuError(error: unknown): void {
|
||||
console.error('菜单处理失败:', error)
|
||||
|
||||
// 清理所有状态
|
||||
const userStore = useUserStore()
|
||||
const menuStore = useMenuStore()
|
||||
const worktabStore = useWorktabStore()
|
||||
|
||||
// 清空菜单和工作台
|
||||
menuStore.clearMenuList()
|
||||
worktabStore.clearAll()
|
||||
|
||||
// 重置路由注册状态
|
||||
isRouteRegistered.value = false
|
||||
|
||||
// 登出用户
|
||||
userStore.logOut()
|
||||
|
||||
throw error instanceof Error ? error : new Error('获取菜单列表失败,请重新登录')
|
||||
}
|
||||
|
||||
@@ -251,20 +286,21 @@ function isValidMenuList(menuList: AppRouteRecord[]): boolean {
|
||||
*/
|
||||
export function resetRouterState(router: Router): void {
|
||||
isRouteRegistered.value = false
|
||||
|
||||
|
||||
// 清理所有动态注册的路由(不仅仅是带有dynamic标记的)
|
||||
const routesToRemove: string[] = []
|
||||
router.getRoutes().forEach((route) => {
|
||||
// 排除静态路由和登录页面
|
||||
if (route.name &&
|
||||
route.name !== 'Login' &&
|
||||
route.name !== 'Exception404' &&
|
||||
route.name !== 'Exception500' &&
|
||||
!route.path.startsWith('/exception/')) {
|
||||
if (
|
||||
route.name &&
|
||||
route.name !== 'Login' &&
|
||||
route.name !== 'Exception404' &&
|
||||
route.name !== 'Exception500' &&
|
||||
!route.path.startsWith('/exception/')
|
||||
) {
|
||||
routesToRemove.push(route.name as string)
|
||||
}
|
||||
})
|
||||
|
||||
// 移除动态路由
|
||||
routesToRemove.forEach((routeName) => {
|
||||
try {
|
||||
@@ -273,11 +309,9 @@ export function resetRouterState(router: Router): void {
|
||||
console.warn(`移除路由失败: ${routeName}`, error)
|
||||
}
|
||||
})
|
||||
|
||||
// 清空菜单数据
|
||||
const menuStore = useMenuStore()
|
||||
menuStore.clearMenuList()
|
||||
|
||||
// 清空工作台标签页
|
||||
const worktabStore = useWorktabStore()
|
||||
worktabStore.clearAll()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { RoutesAlias } from '../routesAlias'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { WEB_LINKS } from '@/utils/constants'
|
||||
|
||||
/**
|
||||
* 菜单列表、异步路由
|
||||
*
|
||||
@@ -13,13 +11,21 @@ import { WEB_LINKS } from '@/utils/constants'
|
||||
* 可以是 i18n 的 key,也可以是字符串,比如:'用户列表'
|
||||
*/
|
||||
export const asyncRoutes: AppRouteRecord[] = [
|
||||
{
|
||||
path: '/admin/inquiry_system',
|
||||
name: 'Inquiry_system',
|
||||
component: RoutesAlias.inquirySystem,
|
||||
meta: {
|
||||
title: '询单系统'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/customer/add',
|
||||
name: 'CrmpcustomerAdd',
|
||||
component: RoutesAlias.CrmpcustomerAdd,
|
||||
meta: {
|
||||
title: '新增客户资料',
|
||||
isHide: true,
|
||||
isHide: true
|
||||
// isHideTab: true
|
||||
}
|
||||
},
|
||||
@@ -29,7 +35,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
component: RoutesAlias.CrmpcustomerShow,
|
||||
meta: {
|
||||
title: '客户资料',
|
||||
isHide: true,
|
||||
isHide: true
|
||||
// isHideTab: true
|
||||
}
|
||||
},
|
||||
@@ -39,7 +45,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
component: RoutesAlias.CrmpcustomerEdit,
|
||||
meta: {
|
||||
title: '编辑客户资料',
|
||||
isHide: true,
|
||||
isHide: true
|
||||
// isHideTab: true
|
||||
}
|
||||
},
|
||||
@@ -49,7 +55,7 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
component: RoutesAlias.CrmpcustomerFollowadd,
|
||||
meta: {
|
||||
title: '添加跟进',
|
||||
isHide: true,
|
||||
isHide: true
|
||||
// isHideTab: true
|
||||
}
|
||||
},
|
||||
@@ -59,7 +65,115 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
component: RoutesAlias.recycle,
|
||||
meta: {
|
||||
title: '回收站',
|
||||
isHide: true,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
// 备货单相关路由
|
||||
{
|
||||
path: '/crmpc/stocking/stocking_list',
|
||||
name: 'StockingList',
|
||||
component: 'crmpc/stocking/stocking_list',
|
||||
meta: {
|
||||
title: '备货单列表',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/stocking/Add',
|
||||
name: 'StockingAdd',
|
||||
component: 'crmpc/stocking/Add',
|
||||
meta: {
|
||||
title: '创建备货单',
|
||||
isHide: true,
|
||||
isHideTab: false,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/stocking/show',
|
||||
name: 'StockingShow',
|
||||
component: 'crmpc/stocking/show',
|
||||
meta: {
|
||||
title: '备货单详情',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/stocking/final_add',
|
||||
name: 'StockingFinalAdd',
|
||||
component: 'crmpc/stocking/final_add',
|
||||
meta: {
|
||||
title: '最终备货单',
|
||||
isHide: true,
|
||||
isHideTab: false,
|
||||
keepAlive: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/stocking/final_show',
|
||||
name: 'StockingFinalShow',
|
||||
component: 'crmpc/stocking/final_show',
|
||||
meta: {
|
||||
title: '最终备货单详情',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
//审批详情
|
||||
{
|
||||
path: '/crmpc/stocking/stocking_auth_detil',
|
||||
name: 'StockingAuth',
|
||||
component: 'crmpc/stocking/stocking_auth_detil',
|
||||
meta: {
|
||||
title: '最终备货单详情',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
// 审批流程添加
|
||||
{
|
||||
path: '/admin/approval/approval_add',
|
||||
name: 'approvalAdd',
|
||||
component: 'admin/approval/approval_add',
|
||||
meta: {
|
||||
title: '审批流程添加',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/crmpc/approval/auth_detil',
|
||||
name: 'approvalDetil',
|
||||
component: 'crmpc/approval/auth_detil',
|
||||
meta: {
|
||||
title: '审核详情',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
// 管理端备货单相关路由
|
||||
{
|
||||
path: '/admin/stock/stocking_list_admin',
|
||||
name: 'AdminStockingList',
|
||||
component: 'admin/stock/stocking_list_admin',
|
||||
meta: {
|
||||
title: '备货单列表',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/stock/show_admin',
|
||||
name: 'AdminStockingShow',
|
||||
component: 'admin/stock/show_admin',
|
||||
meta: {
|
||||
title: '备货单详情',
|
||||
isHide: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/stock/final_show_admin',
|
||||
name: 'AdminStockingFinalShow',
|
||||
component: 'admin/stock/final_show_admin',
|
||||
meta: {
|
||||
title: '最终备货单详情',
|
||||
isHide: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -41,6 +41,12 @@ export const staticRoutes: AppRouteRecordRaw[] = [
|
||||
component: () => import('@/views/auth/feishu-callback.vue'),
|
||||
meta: { title: '飞书回调', noLogin: true, isHideTab: true }
|
||||
},
|
||||
{
|
||||
path: '/auth/signature/mobile',
|
||||
name: 'SignatureMobile',
|
||||
component: () => import('@views/auth/signature/mobile.vue'),
|
||||
meta: { title: '电子签名', isHideTab: true, noLogin: true, setTheme: true }
|
||||
},
|
||||
{
|
||||
path: '/exception',
|
||||
component: Home,
|
||||
|
||||
@@ -54,12 +54,21 @@ export enum RoutesAlias {
|
||||
Crmpchome = '/crmpc/home', // crm首页
|
||||
Crmphighseas = '/crmpc/highseas', // crm公海
|
||||
Crmpcustomer = '/crmpc/customer', // crm客户
|
||||
CrmpcustomerAdd = '/crmpc/customer/Add',//客户添加
|
||||
CrmpcustomerEdit = '/crmpc/customer/Edit',//客户编辑
|
||||
CrmpcustomerShow = '/crmpc/customer/Show',//客户详情
|
||||
CrmpcustomerFollowadd = '/crmpc/customer/Followadd',//添加跟进
|
||||
recycle = '/crmpc/recycle',//回收站
|
||||
CrmpcustomerAdd = '/crmpc/customer/Add', //客户添加
|
||||
CrmpcustomerEdit = '/crmpc/customer/Edit', //客户编辑
|
||||
CrmpcustomerShow = '/crmpc/customer/Show', //客户详情
|
||||
CrmpcustomerFollowadd = '/crmpc/customer/Followadd', //添加跟进
|
||||
recycle = '/crmpc/recycle', //回收站
|
||||
UserCenter = '/system/user-center', // 用户中心
|
||||
inquirySystem = '/admin/inquiry_system', // 询单系统
|
||||
customer = '/admin/customer', // 管理端客户列表
|
||||
customeraudit = '/admin/customer/customeraudit', // 管理端待审客户列表
|
||||
// 备货单相关路由
|
||||
StockingList = '/crmpc/stocking/stocking_list', // 备货单列表
|
||||
StockingAdd = '/crmpc/stocking/Add', // 创建备货单
|
||||
StockingShow = '/crmpc/stocking/show', // 备货单详情
|
||||
StockingFinalAdd = '/crmpc/stocking/final_add', // 最终备货单添加
|
||||
StockingFinalShow = '/crmpc/stocking/final_show' // 最终备货单详情
|
||||
}
|
||||
|
||||
// 主页路由
|
||||
|
||||
@@ -130,15 +130,21 @@ function loadComponent(componentPath: string, routeName: string): () => Promise<
|
||||
}
|
||||
|
||||
// 构建可能的路径
|
||||
const fullPath = `../../views${componentPath}.vue`
|
||||
const fullPathWithIndex = `../../views${componentPath}/index.vue`
|
||||
const fullPathNoSlash = `../../views${componentPath}.vue`
|
||||
const fullPathWithIndexNoSlash = `../../views${componentPath}/index.vue`
|
||||
const fullPathWithSlash = `../../views/${componentPath}.vue`
|
||||
const fullPathWithIndexSlash = `../../views/${componentPath}/index.vue`
|
||||
|
||||
// 先尝试直接路径,再尝试添加/index的路径
|
||||
const module = modules[fullPath] || modules[fullPathWithIndex]
|
||||
// 依次尝试四种可能路径(兼容是否带斜杠的情况)
|
||||
const module =
|
||||
modules[fullPathNoSlash] ||
|
||||
modules[fullPathWithIndexNoSlash] ||
|
||||
modules[fullPathWithSlash] ||
|
||||
modules[fullPathWithIndexSlash]
|
||||
|
||||
if (!module) {
|
||||
console.error(
|
||||
`[路由错误] 未找到组件:${routeName},尝试过的路径: ${fullPath} 和 ${fullPathWithIndex}`
|
||||
`[路由错误] 未找到组件:${routeName},尝试过的路径: ${fullPathNoSlash}、${fullPathWithIndexNoSlash}、${fullPathWithSlash} 和 ${fullPathWithIndexSlash}`
|
||||
)
|
||||
return () =>
|
||||
Promise.resolve({
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { LanguageEnum } from '@/enums/appEnum'
|
||||
import { router } from '@/router'
|
||||
import { useSettingStore } from './setting'
|
||||
import { useWorktabStore } from './worktab'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { setPageTitle } from '@/router/utils/utils'
|
||||
import { resetRouterState } from '@/router/guards/beforeEach'
|
||||
import { RoutesAlias } from '@/router/routesAlias'
|
||||
import { UserService } from '@/api/usersApi' // 新增导入
|
||||
let refreshTimeout: ReturnType<typeof setTimeout> | null = null // 定时器变量
|
||||
|
||||
// 用户
|
||||
export const useUserStore = defineStore(
|
||||
'userStore',
|
||||
() => {
|
||||
const language = ref(LanguageEnum.ZH)
|
||||
const isLogin = ref(false)
|
||||
const isLock = ref(false)
|
||||
const lockPassword = ref('')
|
||||
const info = ref<Partial<Api.User.UserInfo>>({})
|
||||
const searchHistory = ref<AppRouteRecord[]>([])
|
||||
const accessToken = ref('')
|
||||
const refreshToken = ref('')
|
||||
const companyid = ref('')
|
||||
const companyabbr = ref('')
|
||||
const systemsid = ref('')
|
||||
const systemsabbr = ref('')
|
||||
const home_page = ref('')
|
||||
const loginType = ref<'feishu' | 'upwd'>('upwd')
|
||||
const tokenExpireAt = ref<number>(0)
|
||||
|
||||
const getUserInfo = computed(() => info.value)
|
||||
const getSettingState = computed(() => useSettingStore().$state)
|
||||
const getWorktabState = computed(() => useWorktabStore().$state)
|
||||
|
||||
const setUserInfo = (newInfo: Api.User.UserInfo) => {
|
||||
info.value = newInfo
|
||||
}
|
||||
|
||||
const setLoginStatus = (status: boolean) => {
|
||||
isLogin.value = status
|
||||
}
|
||||
|
||||
const setLanguage = (lang: LanguageEnum) => {
|
||||
setPageTitle(router.currentRoute.value)
|
||||
language.value = lang
|
||||
}
|
||||
const setLoginType = (type: 'feishu' | 'upwd') => {
|
||||
loginType.value = type
|
||||
}
|
||||
const setSearchHistory = (list: AppRouteRecord[]) => {
|
||||
searchHistory.value = list
|
||||
}
|
||||
|
||||
const setLockStatus = (status: boolean) => {
|
||||
isLock.value = status
|
||||
}
|
||||
|
||||
const setLockPassword = (password: string) => {
|
||||
lockPassword.value = password
|
||||
}
|
||||
|
||||
const setToken = (newAccessToken: string, newRefreshToken?: string, expiresIn?: number) => {
|
||||
accessToken.value = newAccessToken
|
||||
if (newRefreshToken) {
|
||||
refreshToken.value = newRefreshToken
|
||||
}
|
||||
if (expiresIn) {
|
||||
const expireAt = Date.now() + expiresIn * 1000
|
||||
tokenExpireAt.value = expireAt
|
||||
localStorage.setItem('token_expire_at', String(expireAt))
|
||||
// 检查是否需要重新设置定时器
|
||||
const currentLeftSeconds = Math.floor((expireAt - Date.now()) / 1000)
|
||||
if (currentLeftSeconds > 0) {
|
||||
scheduleTokenRefresh(currentLeftSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
const setCompany = (id: string, abbr: string) => {
|
||||
companyid.value = id
|
||||
companyabbr.value = abbr
|
||||
}
|
||||
const setSystems = (id: string, abbr: string) => {
|
||||
systemsid.value = id
|
||||
systemsabbr.value = abbr
|
||||
}
|
||||
// 设置首页
|
||||
const sethome_page = (page: string) => {
|
||||
home_page.value = page
|
||||
}
|
||||
function restoreTokenTimer() {
|
||||
const expireAt = Number(localStorage.getItem('token_expire_at') || '0')
|
||||
if (accessToken.value && expireAt > Date.now()) {
|
||||
const leftSeconds = Math.floor((expireAt - Date.now()) / 1000)
|
||||
scheduleTokenRefresh(leftSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
let isScheduled = false
|
||||
|
||||
function scheduleTokenRefresh(expiresIn: number) {
|
||||
if (refreshTimeout) {
|
||||
clearTimeout(refreshTimeout)
|
||||
refreshTimeout = null
|
||||
}
|
||||
|
||||
// 提前1分钟刷新
|
||||
const refreshTime = (expiresIn - 60) * 1000
|
||||
|
||||
if (refreshTime > 0) {
|
||||
if (!isScheduled) {
|
||||
isScheduled = true
|
||||
refreshTimeout = setTimeout(() => {
|
||||
refreshTokenAction().finally(() => {
|
||||
isScheduled = false // 重置标志位
|
||||
})
|
||||
}, refreshTime)
|
||||
} else {
|
||||
console.log('scheduleTokenRefresh is already scheduled, skipping...')
|
||||
}
|
||||
} else {
|
||||
console.error('Invalid refreshTime:', refreshTime)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTokenAction() {
|
||||
try {
|
||||
const { data } = await UserService.refreshToken({ refresh_token: refreshToken.value })
|
||||
// 动态计算剩余时间
|
||||
const currentTime = Date.now()
|
||||
const expireAt = currentTime + data.expires_in * 1000
|
||||
const leftSeconds = Math.floor((expireAt - currentTime) / 1000)
|
||||
setToken(data.access_token, data.refresh_token, leftSeconds)
|
||||
} catch (e) {
|
||||
console.error('Token refresh failed:', e)
|
||||
logOut()
|
||||
}
|
||||
}
|
||||
|
||||
const logOut = () => {
|
||||
// 清空所有本地缓存
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
systemsabbr.value = ''
|
||||
systemsid.value = ''
|
||||
companyabbr.value = ''
|
||||
companyid.value = ''
|
||||
home_page.value = ''
|
||||
info.value = {}
|
||||
isLogin.value = false
|
||||
isLock.value = false
|
||||
lockPassword.value = ''
|
||||
accessToken.value = ''
|
||||
refreshToken.value = ''
|
||||
if (refreshTimeout) clearTimeout(refreshTimeout) // 清理定时器
|
||||
// 清理工作台标签页
|
||||
useWorktabStore().clearAll()
|
||||
// 清理sessionStorage中的用户信息缓存
|
||||
sessionStorage.removeItem('lastUserInfo')
|
||||
sessionStorage.removeItem('iframeRoutes')
|
||||
// 重置路由状态
|
||||
resetRouterState(router)
|
||||
// 跳转到登录页
|
||||
router.push(RoutesAlias.Login)
|
||||
}
|
||||
|
||||
return {
|
||||
language,
|
||||
isLogin,
|
||||
isLock,
|
||||
lockPassword,
|
||||
info,
|
||||
companyid,
|
||||
companyabbr,
|
||||
systemsid,
|
||||
systemsabbr,
|
||||
home_page,
|
||||
searchHistory,
|
||||
loginType,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
tokenExpireAt,
|
||||
getUserInfo,
|
||||
getSettingState,
|
||||
getWorktabState,
|
||||
setUserInfo,
|
||||
setLoginStatus,
|
||||
setLanguage,
|
||||
setSearchHistory,
|
||||
setLockStatus,
|
||||
setLockPassword,
|
||||
setToken,
|
||||
setCompany,
|
||||
setSystems,
|
||||
setLoginType,
|
||||
sethome_page,
|
||||
logOut,
|
||||
restoreTokenTimer
|
||||
}
|
||||
},
|
||||
{
|
||||
persist: {
|
||||
key: 'user',
|
||||
storage: localStorage,
|
||||
afterRestore: (ctx: { store: { restoreTokenTimer: () => void } }) => {
|
||||
ctx.store.restoreTokenTimer()
|
||||
}
|
||||
} as any
|
||||
}
|
||||
)
|
||||
@@ -25,6 +25,9 @@ export const useUserStore = defineStore(
|
||||
const refreshToken = ref('')
|
||||
const companyid = ref('')
|
||||
const companyabbr = ref('')
|
||||
const systemsid = ref('')
|
||||
const systemsabbr = ref('')
|
||||
const home_page = ref('')
|
||||
const loginType = ref<'feishu' | 'upwd'>('upwd')
|
||||
const tokenExpireAt = ref<number>(0)
|
||||
|
||||
@@ -65,17 +68,28 @@ export const useUserStore = defineStore(
|
||||
refreshToken.value = newRefreshToken
|
||||
}
|
||||
if (expiresIn) {
|
||||
// 记录绝对过期时间
|
||||
const expireAt = Date.now() + expiresIn * 1000
|
||||
tokenExpireAt.value = expireAt
|
||||
localStorage.setItem('token_expire_at', String(expireAt))
|
||||
scheduleTokenRefresh(expiresIn)
|
||||
// 检查是否需要重新设置定时器
|
||||
const currentLeftSeconds = Math.floor((expireAt - Date.now()) / 1000)
|
||||
if (currentLeftSeconds > 0) {
|
||||
scheduleTokenRefresh(currentLeftSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
const setCompany= (id: string, abbr: string) => {
|
||||
const setCompany = (id: string, abbr: string) => {
|
||||
companyid.value = id
|
||||
companyabbr.value = abbr
|
||||
}
|
||||
const setSystems = (id: string, abbr: string) => {
|
||||
systemsid.value = id
|
||||
systemsabbr.value = abbr
|
||||
}
|
||||
// 设置首页
|
||||
const sethome_page = (page: string) => {
|
||||
home_page.value = page
|
||||
}
|
||||
function restoreTokenTimer() {
|
||||
const expireAt = Number(localStorage.getItem('token_expire_at') || '0')
|
||||
if (accessToken.value && expireAt > Date.now()) {
|
||||
@@ -84,31 +98,56 @@ export const useUserStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
let isScheduled = false
|
||||
|
||||
function scheduleTokenRefresh(expiresIn: number) {
|
||||
if (refreshTimeout) clearTimeout(refreshTimeout)
|
||||
if (refreshTimeout) {
|
||||
clearTimeout(refreshTimeout)
|
||||
refreshTimeout = null
|
||||
}
|
||||
|
||||
// 提前1分钟刷新
|
||||
const refreshTime = (expiresIn - 100) * 1000
|
||||
const refreshTime = (expiresIn - 60) * 1000
|
||||
|
||||
if (refreshTime > 0) {
|
||||
refreshTimeout = setTimeout(refreshTokenAction, refreshTime)
|
||||
if (!isScheduled) {
|
||||
isScheduled = true
|
||||
refreshTimeout = setTimeout(() => {
|
||||
refreshTokenAction().finally(() => {
|
||||
isScheduled = false // 重置标志位
|
||||
})
|
||||
}, refreshTime)
|
||||
} else {
|
||||
console.log('scheduleTokenRefresh is already scheduled, skipping...')
|
||||
}
|
||||
} else {
|
||||
console.error('Invalid refreshTime:', refreshTime)
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshTokenAction() {
|
||||
try {
|
||||
const { data } = await UserService.refreshToken({ refresh_token: refreshToken.value })
|
||||
setToken(data.access_token, data.refresh_token, data.expires_in)
|
||||
// 动态计算剩余时间
|
||||
const currentTime = Date.now()
|
||||
const expireAt = currentTime + data.expires_in * 1000
|
||||
const leftSeconds = Math.floor((expireAt - currentTime) / 1000)
|
||||
setToken(data.access_token, data.refresh_token, leftSeconds)
|
||||
} catch (e) {
|
||||
console.error('Token refresh failed:', e)
|
||||
logOut()
|
||||
}
|
||||
}
|
||||
|
||||
const logOut = () => {
|
||||
// 清空所有本地缓存
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
systemsabbr.value = ''
|
||||
systemsid.value = ''
|
||||
companyabbr.value = ''
|
||||
companyid.value = ''
|
||||
home_page.value = ''
|
||||
info.value = {}
|
||||
isLogin.value = false
|
||||
isLock.value = false
|
||||
@@ -116,17 +155,13 @@ export const useUserStore = defineStore(
|
||||
accessToken.value = ''
|
||||
refreshToken.value = ''
|
||||
if (refreshTimeout) clearTimeout(refreshTimeout) // 清理定时器
|
||||
|
||||
// 清理工作台标签页
|
||||
useWorktabStore().clearAll()
|
||||
|
||||
// 清理sessionStorage中的用户信息缓存
|
||||
sessionStorage.removeItem('lastUserInfo')
|
||||
sessionStorage.removeItem('iframeRoutes')
|
||||
|
||||
// 重置路由状态
|
||||
resetRouterState(router)
|
||||
|
||||
// 跳转到登录页
|
||||
router.push(RoutesAlias.Login)
|
||||
}
|
||||
@@ -139,6 +174,9 @@ export const useUserStore = defineStore(
|
||||
info,
|
||||
companyid,
|
||||
companyabbr,
|
||||
systemsid,
|
||||
systemsabbr,
|
||||
home_page,
|
||||
searchHistory,
|
||||
loginType,
|
||||
accessToken,
|
||||
@@ -155,7 +193,9 @@ export const useUserStore = defineStore(
|
||||
setLockPassword,
|
||||
setToken,
|
||||
setCompany,
|
||||
setSystems,
|
||||
setLoginType,
|
||||
sethome_page,
|
||||
logOut,
|
||||
restoreTokenTimer
|
||||
}
|
||||
@@ -163,7 +203,10 @@ export const useUserStore = defineStore(
|
||||
{
|
||||
persist: {
|
||||
key: 'user',
|
||||
storage: localStorage
|
||||
}
|
||||
storage: localStorage,
|
||||
afterRestore: (ctx: { store: { restoreTokenTimer: () => void } }) => {
|
||||
ctx.store.restoreTokenTimer()
|
||||
}
|
||||
} as any
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { HOME_PAGE } from '@/router/routesAlias'
|
||||
import { router } from '@/router'
|
||||
import { LocationQueryRaw, Router } from 'vue-router'
|
||||
import { WorkTab } from '@/types'
|
||||
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
interface WorktabState {
|
||||
current: Partial<WorkTab>
|
||||
opened: WorkTab[]
|
||||
@@ -144,6 +144,7 @@ export const useWorktabStore = defineStore(
|
||||
* 关闭指定的选项卡
|
||||
*/
|
||||
const removeTab = (path: string): void => {
|
||||
const { home_page } = useUserStore()
|
||||
const targetTab = getTab(path)
|
||||
const targetIndex = findTabIndex(path)
|
||||
|
||||
@@ -167,9 +168,9 @@ export const useWorktabStore = defineStore(
|
||||
|
||||
// 如果关闭后无标签页,跳转首页
|
||||
if (!hasOpenedTabs.value) {
|
||||
if (path !== HOME_PAGE) {
|
||||
if (path !== home_page) {
|
||||
current.value = {}
|
||||
safeRouterPush({ path: HOME_PAGE })
|
||||
safeRouterPush({ path: home_page })
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -287,12 +288,12 @@ export const useWorktabStore = defineStore(
|
||||
*/
|
||||
const removeAll = (): void => {
|
||||
const hasFixedTabs = opened.value.some((tab) => tab.fixedTab)
|
||||
|
||||
const { home_page } = useUserStore()
|
||||
// 获取可关闭的标签页
|
||||
const closableTabs = opened.value.filter((tab) => {
|
||||
if (!isTabClosable(tab)) return false
|
||||
// 如果有固定标签,则所有可关闭的都可以关闭;否则保留首页
|
||||
return hasFixedTabs || tab.path !== HOME_PAGE
|
||||
return hasFixedTabs || tab.path !== home_page
|
||||
})
|
||||
|
||||
if (closableTabs.length === 0) {
|
||||
@@ -305,18 +306,18 @@ export const useWorktabStore = defineStore(
|
||||
|
||||
// 保留不可关闭的标签页和首页(当没有固定标签时)
|
||||
opened.value = opened.value.filter((tab) => {
|
||||
return !isTabClosable(tab) || (!hasFixedTabs && tab.path === HOME_PAGE)
|
||||
return !isTabClosable(tab) || (!hasFixedTabs && tab.path === home_page)
|
||||
})
|
||||
|
||||
// 处理激活状态
|
||||
if (!hasOpenedTabs.value) {
|
||||
current.value = {}
|
||||
safeRouterPush({ path: HOME_PAGE })
|
||||
safeRouterPush({ path: home_page })
|
||||
return
|
||||
}
|
||||
|
||||
// 选择激活的标签页:优先首页,其次第一个可用标签
|
||||
const homeTab = opened.value.find((tab) => tab.path === HOME_PAGE)
|
||||
const homeTab = opened.value.find((tab) => tab.path === home_page)
|
||||
const targetTab = homeTab || opened.value[0]
|
||||
|
||||
current.value = targetTab
|
||||
|
||||
@@ -9,7 +9,7 @@ declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElButton: (typeof import('element-plus/es'))['ElButton']
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElMessageBox: (typeof import('element-plus/es'))['ElMessageBox']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
const ElNotification: (typeof import('element-plus/es'))['ElNotification']
|
||||
const ElPopconfirm: (typeof import('element-plus/es'))['ElPopconfirm']
|
||||
const ElPopover: (typeof import('element-plus/es'))['ElPopover']
|
||||
|
||||
@@ -7,6 +7,7 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ApprovalTimeline: typeof import('./../components/custom/ApprovalTimeline.vue')['default']
|
||||
ArtBackToTop: typeof import('./../components/core/base/art-back-to-top/index.vue')['default']
|
||||
ArtBarChart: typeof import('./../components/core/charts/ArtBarChart.vue')['default']
|
||||
ArtBarChartCard: typeof import('./../components/core/cards/art-bar-chart-card/index.vue')['default']
|
||||
@@ -72,12 +73,16 @@ declare module 'vue' {
|
||||
CommentItem: typeof import('./../components/custom/comment-widget/widget/CommentItem.vue')['default']
|
||||
CommentWidget: typeof import('./../components/custom/comment-widget/index.vue')['default']
|
||||
ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default']
|
||||
copy: typeof import('./../components/custom/ProcurementDetailView copy.vue')['default']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElBadge: typeof import('element-plus/es')['ElBadge']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||
ElCalendar: typeof import('element-plus/es')['ElCalendar']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
@@ -93,8 +98,10 @@ declare module 'vue' {
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||
ElLink: typeof import('element-plus/es')['ElLink']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
@@ -117,16 +124,21 @@ declare module 'vue' {
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElText: typeof import('element-plus/es')['ElText']
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
ElWatermark: typeof import('element-plus/es')['ElWatermark']
|
||||
HorizontalSubmenu: typeof import('./../components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue')['default']
|
||||
LoginLeftView: typeof import('./../components/core/views/login/LoginLeftView.vue')['default']
|
||||
MenuLayoutSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue')['default']
|
||||
MenuStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue')['default']
|
||||
OrderDetailContent: typeof import('./../components/custom/OrderDetailContent.vue')['default']
|
||||
ProcurementDetailView: typeof import('./../components/custom/ProcurementDetailView.vue')['default']
|
||||
'ProcurementDetailView copy': typeof import('./../components/custom/ProcurementDetailView copy.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SectionTitle: typeof import('./../components/core/layouts/art-settings-panel/widget/SectionTitle.vue')['default']
|
||||
@@ -135,6 +147,7 @@ declare module 'vue' {
|
||||
SettingItem: typeof import('./../components/core/layouts/art-settings-panel/widget/SettingItem.vue')['default']
|
||||
SidebarSubmenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue')['default']
|
||||
ThemeSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ThemeSettings.vue')['default']
|
||||
WorkChatDrawer: typeof import('./../components/custom/chat/WorkChatDrawer.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
|
||||
authList?: Array<{
|
||||
title: string
|
||||
auth_mark: string
|
||||
authMark?: string // 兼容旧版
|
||||
}>
|
||||
/** 是否为一级菜单 */
|
||||
isFirstLevel?: boolean
|
||||
|
||||
@@ -29,6 +29,32 @@ declare namespace Api {
|
||||
|
||||
/** 启用状态 */
|
||||
type EnableStatus = '1' | '2'
|
||||
|
||||
interface CommonInfo {
|
||||
data: never[]
|
||||
}
|
||||
|
||||
/** 列表数据 */
|
||||
interface CommonListData {
|
||||
records: CommonInfo[]
|
||||
current: number
|
||||
size: number
|
||||
total: number
|
||||
}
|
||||
interface CommonListDatad {
|
||||
data: CommonInfo[]
|
||||
current: number
|
||||
size: number
|
||||
total: number
|
||||
}
|
||||
interface AddResponse {
|
||||
code: string
|
||||
message: string
|
||||
}
|
||||
interface CommonListDatad1 {
|
||||
data: never[]
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
/** 认证类型 */
|
||||
@@ -116,17 +142,19 @@ declare namespace Api {
|
||||
interface AddParams {
|
||||
categorie_id: any
|
||||
content: any
|
||||
attachment:any
|
||||
attachment: any
|
||||
}
|
||||
interface AddResponse {
|
||||
code: string
|
||||
message: string
|
||||
status: any
|
||||
data?: any
|
||||
}
|
||||
/** 修改状态参数 */
|
||||
interface StatusParams {
|
||||
id: any
|
||||
msg: any
|
||||
status:any
|
||||
status: any
|
||||
}
|
||||
//分类
|
||||
interface CategorieInfo {
|
||||
@@ -165,6 +193,8 @@ declare namespace Api {
|
||||
|
||||
/** 列表数据 */
|
||||
interface CrmListData {
|
||||
country_id: any
|
||||
channel_id: any
|
||||
data: CrmInfo[]
|
||||
current: number
|
||||
size: number
|
||||
|
||||
@@ -47,9 +47,10 @@ axiosInstance.interceptors.request.use(
|
||||
const { language } = useUserStore()
|
||||
const { loginType } = useUserStore()
|
||||
const { companyabbr } = useUserStore()
|
||||
const { systemsabbr } = useUserStore()
|
||||
request.headers.set('Fdesk-Language', language)
|
||||
request.headers.set('Fdesk-Corp-Abbr', companyabbr)
|
||||
request.headers.set('Fdesk-System-Code', 'fdesk')
|
||||
request.headers.set('Fdesk-System-Code', systemsabbr)
|
||||
request.headers.set('Fdesk-Login-Type', loginType)
|
||||
// 设置 token 和 请求头
|
||||
if (accessToken) {
|
||||
|
||||
@@ -98,10 +98,10 @@ class StorageCompatibilityManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在登录页面
|
||||
* 检查是否在登录页面或电子签名页面
|
||||
*/
|
||||
private isOnLoginPage(): boolean {
|
||||
return location.href.includes(RoutesAlias.Login)
|
||||
return location.href.includes(RoutesAlias.Login) || location.href.includes('/auth/signature/mobile')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import mittBus from './mittBus'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { ElNotification } from 'element-plus'
|
||||
|
||||
type JsonValue = Record<string, any> | any[]
|
||||
|
||||
const WS_BASE = import.meta.env.VITE_API_URL_WSS + '/ws?token='
|
||||
const MAX_RECONNECT_ATTEMPTS = 10
|
||||
const MAX_RECONNECT_DELAY = 50000
|
||||
const HEARTBEAT_INTERVAL = 50000
|
||||
|
||||
let socket: WebSocket | null = null
|
||||
let reconnectTimer: any = null
|
||||
let heartbeatTimer: any = null
|
||||
let reconnectAttempts = 0
|
||||
let isInitialConnection = true
|
||||
let isConnecting = false
|
||||
let lastInitToken: string | null = null
|
||||
let lastInitAt = 0
|
||||
const INIT_DEBOUNCE_MS = 1200
|
||||
|
||||
// 生成唯一ID
|
||||
const generateTraceId = (): string => {
|
||||
try {
|
||||
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||
return `req-${crypto.randomUUID()}`
|
||||
}
|
||||
} catch {
|
||||
// 忽略错误,使用fallback方案
|
||||
}
|
||||
const timestamp = Date.now().toString(36)
|
||||
const random = Math.random().toString(36).slice(2)
|
||||
return `req-${timestamp}-${random}`
|
||||
}
|
||||
|
||||
// 计算重连延迟
|
||||
const getReconnectDelay = () => {
|
||||
const base = 1000 * Math.pow(2, reconnectAttempts)
|
||||
return Math.min(base, MAX_RECONNECT_DELAY)
|
||||
}
|
||||
|
||||
// 清理定时器
|
||||
const cleanup = () => {
|
||||
if (reconnectTimer) {
|
||||
clearTimeout(reconnectTimer)
|
||||
reconnectTimer = null
|
||||
}
|
||||
if (heartbeatTimer) {
|
||||
clearTimeout(heartbeatTimer)
|
||||
heartbeatTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 启动心跳
|
||||
const startHeartbeat = () => {
|
||||
if (heartbeatTimer) {
|
||||
clearTimeout(heartbeatTimer)
|
||||
}
|
||||
heartbeatTimer = setTimeout(() => {
|
||||
try {
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
const msg = {
|
||||
event: 'system.ping',
|
||||
data: {},
|
||||
trace_id: generateTraceId()
|
||||
}
|
||||
socket.send(JSON.stringify(msg))
|
||||
}
|
||||
} catch {
|
||||
// 忽略心跳发送错误
|
||||
}
|
||||
startHeartbeat()
|
||||
}, HEARTBEAT_INTERVAL)
|
||||
}
|
||||
|
||||
// 发送心跳消息
|
||||
const sendPing = () => {
|
||||
try {
|
||||
if (socket?.readyState === WebSocket.OPEN) {
|
||||
const msg = {
|
||||
event: 'system.ping',
|
||||
data: {},
|
||||
trace_id: generateTraceId()
|
||||
}
|
||||
socket.send(JSON.stringify(msg))
|
||||
}
|
||||
} catch {
|
||||
// 忽略发送错误
|
||||
}
|
||||
}
|
||||
|
||||
// 处理WebSocket消息
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
let data: any = event.data
|
||||
try {
|
||||
data = JSON.parse(event.data)
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
console.log('WebSocket message:', data)
|
||||
if (data.event == 'notify.receive') {
|
||||
ElNotification({
|
||||
title: data.data.notice_data.message.title || '通知',
|
||||
message: data.data.notice_data.message.content,
|
||||
type: 'primary'
|
||||
})
|
||||
mittBus.emit('notification-data-changed', 1)
|
||||
}
|
||||
mittBus.emit('ws:message', data)
|
||||
}
|
||||
|
||||
// 设置WebSocket事件处理器
|
||||
const setupWebSocketHandlers = (ws: WebSocket) => {
|
||||
ws.onopen = () => {
|
||||
isInitialConnection = false
|
||||
isConnecting = false
|
||||
startHeartbeat()
|
||||
mittBus.emit('ws:open')
|
||||
sendPing()
|
||||
}
|
||||
|
||||
ws.onmessage = handleMessage
|
||||
|
||||
ws.onerror = (err) => {
|
||||
mittBus.emit('ws:error', err)
|
||||
isConnecting = false
|
||||
try {
|
||||
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
||||
ws.close()
|
||||
}
|
||||
} catch {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
mittBus.emit('ws:close')
|
||||
isConnecting = false
|
||||
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||
scheduleReconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 安排重连
|
||||
const scheduleReconnect = () => {
|
||||
// 每次安排重连都递增(即使从未成功连接过)
|
||||
reconnectAttempts += 1
|
||||
|
||||
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
console.warn(`WebSocket重连失败,已达到最大重连次数(${MAX_RECONNECT_ATTEMPTS}),停止重连`)
|
||||
mittBus.emit('ws:max-reconnect-reached')
|
||||
return
|
||||
}
|
||||
|
||||
cleanup()
|
||||
const delay = getReconnectDelay()
|
||||
console.log(`WebSocket重连尝试 ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS},延迟: ${delay}ms`)
|
||||
reconnectTimer = setTimeout(() => {
|
||||
const { accessToken } = useUserStore()
|
||||
if (accessToken && !isConnecting) {
|
||||
try {
|
||||
if (socket) {
|
||||
try {
|
||||
socket.close()
|
||||
} catch {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
socket = null
|
||||
}
|
||||
|
||||
const url = `${WS_BASE}${accessToken}`
|
||||
isConnecting = true
|
||||
socket = new WebSocket(url)
|
||||
setupWebSocketHandlers(socket)
|
||||
} catch (e) {
|
||||
mittBus.emit('ws:error', e)
|
||||
isConnecting = false
|
||||
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
|
||||
scheduleReconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, delay)
|
||||
}
|
||||
|
||||
// 初始化WebSocket连接
|
||||
export const initWebSocket = (token: string) => {
|
||||
try {
|
||||
if (!token) return
|
||||
const now = Date.now()
|
||||
// 简单防抖:在短时间内重复触发则跳过
|
||||
if (now - lastInitAt < INIT_DEBOUNCE_MS) {
|
||||
return
|
||||
}
|
||||
// 避免并发初始化:已在连接中或已连接且 token 未变化则跳过
|
||||
if (isConnecting) return
|
||||
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
|
||||
if (lastInitToken === token) {
|
||||
return
|
||||
}
|
||||
// token 变更:需要切换连接
|
||||
try { socket.close() } catch {}
|
||||
socket = null
|
||||
}
|
||||
|
||||
cleanup()
|
||||
|
||||
const url = `${WS_BASE}${token}`
|
||||
isConnecting = true
|
||||
lastInitToken = token
|
||||
lastInitAt = now
|
||||
socket = new WebSocket(url)
|
||||
setupWebSocketHandlers(socket)
|
||||
} catch (e) {
|
||||
mittBus.emit('ws:error', e)
|
||||
isConnecting = false
|
||||
scheduleReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭WebSocket连接
|
||||
export const closeWebSocket = () => {
|
||||
cleanup()
|
||||
try {
|
||||
socket?.close()
|
||||
} catch {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
socket = null
|
||||
reconnectAttempts = 0
|
||||
isInitialConnection = true
|
||||
isConnecting = false
|
||||
lastInitToken = null
|
||||
}
|
||||
|
||||
// 发送WebSocket消息
|
||||
export const sendWebSocket = (payload: string | JsonValue) => {
|
||||
if (!socket || socket.readyState !== WebSocket.OPEN) return false
|
||||
try {
|
||||
const data = typeof payload === 'string' ? payload : JSON.stringify(payload)
|
||||
socket.send(data)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置重连次数
|
||||
export const resetWebSocketReconnect = () => {
|
||||
reconnectAttempts = 0
|
||||
isInitialConnection = true
|
||||
console.log('WebSocket重连次数已重置')
|
||||
}
|
||||
|
||||
// 获取当前重连次数
|
||||
export const getWebSocketReconnectAttempts = () => reconnectAttempts
|
||||
|
||||
// 获取最大重连次数
|
||||
export const getMaxReconnectAttempts = () => MAX_RECONNECT_ATTEMPTS
|
||||
|
||||
// 获取WebSocket连接状态
|
||||
export const getWebSocketStatus = () => ({
|
||||
socket,
|
||||
readyState: socket?.readyState,
|
||||
reconnectAttempts,
|
||||
maxReconnectAttempts: MAX_RECONNECT_ATTEMPTS,
|
||||
isReconnecting: reconnectTimer !== null,
|
||||
isHeartbeating: heartbeatTimer !== null,
|
||||
isInitialConnection,
|
||||
isConnecting,
|
||||
lastInitToken
|
||||
})
|
||||
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="color-page">
|
||||
<div class="color-toolbar boxshadow">
|
||||
<div class="color-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input
|
||||
style="min-width: 120px"
|
||||
v-model="keyword"
|
||||
placeholder="请输入关键字"
|
||||
clearable
|
||||
@keyup.enter="getColorList"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<ElRow>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton @click="addfn()" v-ripple v-if="hasAuth('approval_list@add')" type="primary">
|
||||
新增审核流程
|
||||
</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="name" />
|
||||
<ElTableColumn label="标识" prop="type" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.is_active ? 'success' : 'info'">
|
||||
{{ scope.row.is_active ? '启用' : '停用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" />
|
||||
<ElTableColumn fixed="right" label="操作" width="140px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteColor(scope.row)"
|
||||
v-if="hasAuth('approval_list@destroy')"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog v-model="dialogVisible" title="新增审批流程" align-center width="600px">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="请输入名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="标识" prop="type">
|
||||
<ElInput v-model="form.type" placeholder="请输入标识" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="超时时间" prop="timeout_hours">
|
||||
<ElInput v-model="form.timeout_hours" placeholder="请输入超时时间" >
|
||||
<template #suffix>
|
||||
<span>h</span>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="超时动作" prop="timeout_action">
|
||||
<el-select v-model="form.timeout_action" placeholder="请选择颜色类型" style="width: 100%">
|
||||
<el-option label="外观颜色" :value="1" />
|
||||
<el-option label="内饰颜色" :value="2" />
|
||||
</el-select>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.is_active" :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 { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { router } from '@/router'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'AdminApprovalList' })
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const keyword = ref<string>('')
|
||||
const typeFilter = ref<number | undefined>(undefined)
|
||||
const statusFilter = ref<number | undefined>(undefined)
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<any>({
|
||||
name: '',
|
||||
type: '',
|
||||
timeout_hours: '',
|
||||
timeout_action: '',
|
||||
is_active: 1
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
getColorList()
|
||||
})
|
||||
|
||||
// 监听搜索条件变化
|
||||
watch([keyword, typeFilter, statusFilter], () => {
|
||||
pagination.currentPage = 1
|
||||
getColorList()
|
||||
})
|
||||
// 新增按钮点击
|
||||
const addfn = () => {
|
||||
router.push('/admin/approval/approval_add')
|
||||
}
|
||||
// 获取颜色列表
|
||||
const getColorList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await manageService.getstocklist(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取颜色列表失败:', error)
|
||||
ElMessage.error('获取颜色列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索条件
|
||||
const resetSearch = () => {
|
||||
keyword.value = ''
|
||||
typeFilter.value = undefined
|
||||
statusFilter.value = undefined
|
||||
pagination.currentPage = 1
|
||||
getColorList()
|
||||
}
|
||||
|
||||
// 处理分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getColorList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getColorList()
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
router.push(`/admin/approval/approval_edit?id=${row.id}`)
|
||||
}
|
||||
|
||||
// 删除
|
||||
const deleteColor = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该审核流程吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const { message } = await manageService.stockapprovaldelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getColorList()
|
||||
} 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 {
|
||||
// 新增
|
||||
const { message } = await manageService.stockapproval(form)
|
||||
ElMessage.success(message)
|
||||
dialogVisible.value = false
|
||||
getColorList()
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.color-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.color-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.color-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.el-col2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,504 @@
|
||||
<template>
|
||||
<div class="stocking-page">
|
||||
<div class="stocking-toolbar boxshadow">
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="getList"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getList"><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="flows_name" min-width="140" />
|
||||
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
|
||||
<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="status_desc" 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">
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看审批
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="scope.row.status == 'pending'"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleCreateRepair(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 快速审批
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 快速审批弹出框 -->
|
||||
<el-dialog
|
||||
v-model="approvalDialogVisible"
|
||||
:title="orderDialog.title"
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="approval-dialog"
|
||||
>
|
||||
<div class="approval-content">
|
||||
<!-- 头部信息 -->
|
||||
<div v-if="orderDialog.type == 'stock_orders'">
|
||||
<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"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<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 { useAuth } from '@/composables/useAuth'
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
|
||||
import type { OrderData } from '@/composables/useOrderDetailDialog'
|
||||
import ProcurementDetailView from '@/components/custom/ProcurementDetailView.vue'
|
||||
defineOptions({ name: 'AdminStockingPage' })
|
||||
const zjDialogVisible = ref(false)
|
||||
const orderDialog = reactive({
|
||||
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 || []
|
||||
}
|
||||
const zjForm = reactive({
|
||||
to_uid: '',
|
||||
instance_id: '',
|
||||
step_index: '',
|
||||
reason: ''
|
||||
})
|
||||
// 审批弹出框相关
|
||||
const approvalDialogVisible = ref(false)
|
||||
|
||||
const approvalNotes = ref('')
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
scene: 'todo',
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await manageService.stockapprovalrecord(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
getList()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getList()
|
||||
}
|
||||
const handleView = (row: any) => {
|
||||
router.push({
|
||||
path: '/admin/approval/stocking_auth_detil',
|
||||
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 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) => {
|
||||
activerow.value = row
|
||||
instance_id.value = row.instance_id
|
||||
const res = await manageService.stockapprovaldetailborder({
|
||||
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 = '合塑实业-汽车事业部-采购单(采购)'
|
||||
}
|
||||
approvalDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 审批操作
|
||||
const handleApprove = async (approvalAction: string) => {
|
||||
try {
|
||||
// 这里调用审批API
|
||||
const { message } = await manageService.stockapprovalpass({
|
||||
instance_id: instance_id.value,
|
||||
action: approvalAction,
|
||||
comment: approvalNotes.value
|
||||
})
|
||||
|
||||
ElMessage.success(message)
|
||||
approvalDialogVisible.value = false
|
||||
approvalNotes.value = ''
|
||||
instance_id.value = ''
|
||||
// 刷新列表
|
||||
getList()
|
||||
} catch (error) {
|
||||
console.error('审批失败:', error)
|
||||
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 manageService.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">
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.stocking-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.stocking-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// 审批弹出框样式
|
||||
.approval-dialog {
|
||||
.approval-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
color: #333333;
|
||||
.mr40 { margin-right: 40px; }
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin: 24px 0px;
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: #409eff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-items {
|
||||
.order-item {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #fafafa;
|
||||
|
||||
.item-header {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
|
||||
.item-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.item-details {
|
||||
.detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.summary-item {
|
||||
.summary-label {
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.summary-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-notes {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
gap: 12px;
|
||||
|
||||
.reject-btn {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
background: white;
|
||||
|
||||
&:hover {
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
|
||||
.approve-btn {
|
||||
background: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,642 @@
|
||||
<template>
|
||||
<div class="stocking-show-page">
|
||||
<div class="boxshadow">
|
||||
<div v-if="flow_type == 'stock_orders'">
|
||||
<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>
|
||||
</div>
|
||||
<div class="file-preview">
|
||||
<div v-if="contractFile" class="contract-file">
|
||||
<div v-if="isImageFile(contractFile)" class="image-preview">
|
||||
<img
|
||||
:src="contractFile.url"
|
||||
:alt="contractFile.name"
|
||||
class="preview-image"
|
||||
@click="previewImage(contractFile)"
|
||||
/>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ contractFile.name }}</span>
|
||||
<el-button type="text" size="small" @click="previewImage(contractFile)">
|
||||
预览
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="document-preview">
|
||||
<div class="file-icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ contractFile.name }}</span>
|
||||
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
|
||||
下载
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-file">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>暂无合同文件</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<div class="form-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">审批记录</span>
|
||||
</div>
|
||||
<div class="approval-timeline">
|
||||
<div v-for="(rec, idx) in approvalRecords" :key="idx" class="timeline-item">
|
||||
<div class="left">
|
||||
<div class="avatar-box">
|
||||
<span class="initial">{{ rec.name?.slice(0, 1) || '—' }}</span>
|
||||
<i v-if="rec.approved" class="status-dot iconfont-sys"></i>
|
||||
<i v-if="rec.rejected" class="status-dot rejected"></i>
|
||||
<i v-else-if="rec.pending" class="status-dot pending iconfont-sys"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="row name-role">
|
||||
<span class="name">{{ rec.name }}</span>
|
||||
<span class="role">({{ rec.role || rec.type || '审批人' }})</span>
|
||||
</div>
|
||||
<div class="row time">{{ rec.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!approvalRecords.length" class="empty">暂无审批记录</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow" v-if="status == 'pending'">
|
||||
<div class="form-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">审批</span>
|
||||
</div>
|
||||
<div class="">
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="备注:">
|
||||
<el-input
|
||||
v-model="approvalNotes"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="请输入备注信息"
|
||||
class="approval-notes"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
>
|
||||
<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 预览弹窗 -->
|
||||
<el-dialog
|
||||
v-model="pdf.visible"
|
||||
title="打印预览"
|
||||
width="80%"
|
||||
top="40px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px">
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px">
|
||||
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
|
||||
<el-button @click="download">下载</el-button>
|
||||
</div>
|
||||
<iframe
|
||||
v-if="pdf.blobUrl || pdf.url"
|
||||
ref="frameRef"
|
||||
:src="pdf.blobUrl || pdf.url"
|
||||
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px"
|
||||
/>
|
||||
</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 { commonService } from '@/api/commonApi'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { usePdfPreview } from '@/composables/usePdfPreview'
|
||||
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
|
||||
import ProcurementDetailView from '@/components/custom/ProcurementDetailView.vue'
|
||||
import type { OrderData } from '@/composables/useOrderDetailDialog'
|
||||
import { router } from '@/router'
|
||||
defineOptions({ name: 'AdminStockingAuthDetil' })
|
||||
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 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 manageService.stockapprovaltransfer(zjForms)
|
||||
ElMessage.success(message)
|
||||
router.push('/admin/approval/stocking_auth')
|
||||
} catch (error) {
|
||||
console.error('转交失败:', error)
|
||||
// ElMessage.error('转交失败')
|
||||
}
|
||||
}
|
||||
const handleSubmit = async (approvalAction: string) => {
|
||||
try {
|
||||
const { message } = await manageService.stockapprovalpass({
|
||||
instance_id: instance_id,
|
||||
action: approvalAction,
|
||||
comment: approvalNotes.value
|
||||
})
|
||||
router.push('/admin/approval/stocking_auth')
|
||||
ElMessage.success(message)
|
||||
} catch {
|
||||
ElMessage.error('提交失败')
|
||||
}
|
||||
}
|
||||
// 审批记录(先填充假数据展示)
|
||||
const approvalRecords = ref<any[]>([])
|
||||
const getDictItemSelectlist = ref<any[]>([])
|
||||
const getDictItemSelect = async () => {
|
||||
const res = await commonService.getDictItemSelect({ dict_id: '18' })
|
||||
getDictItemSelectlist.value = res
|
||||
}
|
||||
|
||||
// 合同文件
|
||||
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 manageService.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 = {
|
||||
name: '合同文件',
|
||||
url: res.contract_url || res.contract,
|
||||
file_name: res.contract
|
||||
}
|
||||
}
|
||||
// 审批记录(兼容多种字段)
|
||||
const resstockapprovallogs = await manageService.stockapprovallogs({ instance_id: instance_id })
|
||||
console.log(resstockapprovallogs)
|
||||
const historyList = resstockapprovallogs
|
||||
if (historyList.length > 0) {
|
||||
approvalRecords.value = historyList.map((h: any) => ({
|
||||
name: h?.initiator_name ? h?.initiator_name : h?.name,
|
||||
role: h?.index == -1 ? '申请人' : '审批人',
|
||||
time: h?.finished_at || '',
|
||||
approved: h?.status === 'approved' || h?.status === 'submitted',
|
||||
rejected: h?.status === 'rejected',
|
||||
pending: h?.status != 'approved' && h?.status != 'rejected' && h?.status != 'submitted'
|
||||
}))
|
||||
}
|
||||
}
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 检查文件名
|
||||
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查file_name字段
|
||||
if (file.file_name && file.file_name.startsWith('image/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查URL中的文件扩展名(去除查询参数)
|
||||
if (file.url) {
|
||||
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
|
||||
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 创建图片预览弹窗
|
||||
const imagePreview = document.createElement('div')
|
||||
imagePreview.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const image = document.createElement('img')
|
||||
image.src = file.url
|
||||
image.alt = file.name
|
||||
image.style.cssText = `
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`
|
||||
|
||||
const closeBtn = document.createElement('div')
|
||||
closeBtn.innerHTML = '×'
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
`
|
||||
|
||||
imagePreview.appendChild(image)
|
||||
imagePreview.appendChild(closeBtn)
|
||||
document.body.appendChild(imagePreview)
|
||||
|
||||
// 点击关闭
|
||||
const closePreview = () => {
|
||||
document.body.removeChild(imagePreview)
|
||||
}
|
||||
|
||||
imagePreview.addEventListener('click', closePreview)
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
closePreview()
|
||||
})
|
||||
|
||||
// ESC键关闭
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closePreview()
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = file.url
|
||||
link.download = file.name
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDictItemSelect()
|
||||
getdetilfn()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stocking-show-page {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 12px;
|
||||
color: #333333;
|
||||
.header-left {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.sub {
|
||||
color: #333333;
|
||||
}
|
||||
.mr40 {
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
.form-title {
|
||||
font-weight: bold;
|
||||
color: #222;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.form-title .title {
|
||||
margin-left: 6px;
|
||||
}
|
||||
.items-wrap {
|
||||
margin-top: 6px;
|
||||
}
|
||||
.item-row {
|
||||
padding: 8px 0;
|
||||
}
|
||||
:deep(.el-input.is-disabled .el-input__inner) {
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
padding: 20px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.contract-file {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
object-fit: contain;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.preview-image:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.document-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #409eff;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.no-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-file i {
|
||||
font-size: 48px;
|
||||
color: #ddd;
|
||||
}
|
||||
.submit-wrap {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 24px 0 8px;
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* 审批记录 时间线 */
|
||||
.approval-timeline {
|
||||
padding: 8px 4px 8px 0;
|
||||
}
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.timeline-item + .timeline-item {
|
||||
margin-top: 50px;
|
||||
}
|
||||
.left {
|
||||
width: 46px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.avatar-box {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
background: #3d7bff;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
}
|
||||
.status-dot {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
// width: 14px;
|
||||
// height: 14px;
|
||||
font-size: 9px;
|
||||
padding: 3px 1px;
|
||||
border-radius: 50%;
|
||||
background: #22c55e;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.status-dot.pending {
|
||||
background: #3471ff;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
.status-dot.rejected {
|
||||
background: #ff0000;
|
||||
padding: 3px 3px;
|
||||
}
|
||||
/* 连接下一个节点的虚线 */
|
||||
.timeline-item:not(:last-child) .left::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 52px; /* avatar 46px + 间距 6px */
|
||||
left: 23px; /* 盒子宽度一半,确保竖线居中 */
|
||||
width: 0;
|
||||
height: calc(100% + 16px); /* 加上相邻项之间的间距 */
|
||||
border-left: 2px dotted #bfbfbf;
|
||||
}
|
||||
.right {
|
||||
flex: 1;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.row.name-role {
|
||||
color: #222;
|
||||
}
|
||||
.name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.role {
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.time {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.empty {
|
||||
color: #999;
|
||||
padding: 8px 0 4px 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,406 @@
|
||||
<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"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="getList"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getList"><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="flows_name" min-width="140" />
|
||||
<ElTableColumn label="审批说明" prop="biz_desc" min-width="160" />
|
||||
<ElTableColumn label="审批结果" prop="action_desc" min-width="160" />
|
||||
<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="status_desc" min-width="120" />
|
||||
<ElTableColumn label="审批时间" prop="approved_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">
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看审批
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="scope.row.status == 'pending'"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleCreateRepair(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 快速审批
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 快速审批弹出框 -->
|
||||
<el-dialog
|
||||
v-model="approvalDialogVisible"
|
||||
title="合塑实业-汽车事业部-备货单(客户下定)"
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="approval-dialog"
|
||||
>
|
||||
<div class="approval-content">
|
||||
<!-- 头部信息 -->
|
||||
<OrderDetailContent :orderData="orderDialog.orderData" />
|
||||
<!-- 审批备注 -->
|
||||
<div class="info-section">
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="24">
|
||||
<el-input
|
||||
v-model="approvalNotes"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="备注"
|
||||
class="approval-notes"
|
||||
/>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { router } from '@/router'
|
||||
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: 'AdminStockingPage' })
|
||||
const orderDialog = reactive({
|
||||
orderData: {} as OrderData
|
||||
})
|
||||
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 approvalDialogVisible = ref(false)
|
||||
const stockingInfo = ref<any>({})
|
||||
|
||||
// 回显视图模型,字段与模板一致
|
||||
const header = reactive({ order_no: '', salesman: '' })
|
||||
const base = reactive({
|
||||
pn_no: '',
|
||||
date: '',
|
||||
close_date: '',
|
||||
channel: '',
|
||||
customer_name: '',
|
||||
client_country: '',
|
||||
total_cif: '',
|
||||
total_cif_currency: '',
|
||||
contract_rate: ''
|
||||
})
|
||||
const items = ref<any[]>([])
|
||||
|
||||
const approvalNotes = ref('')
|
||||
const approvalAction = ref('')
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
scene: 'history',
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await manageService.stockapprovalrecord(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
getList()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getList()
|
||||
}
|
||||
const handleView = (row: any) => {
|
||||
router.push({
|
||||
path: '/admin/approval/stocking_auth_detil',
|
||||
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 instance_id = ref('')
|
||||
const handleCreateRepair = async (row: any) => {
|
||||
instance_id.value = row.instance_id
|
||||
const res = await manageService.checkStock({ id: row.biz_id })
|
||||
const orderData: OrderData = res
|
||||
orderDialog.orderData = orderData
|
||||
approvalDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 审批操作
|
||||
const handleApprove = async (approvalAction: string) => {
|
||||
try {
|
||||
// 这里调用审批API
|
||||
const { message } = await manageService.stockapprovalpass({
|
||||
instance_id: instance_id.value,
|
||||
action: approvalAction,
|
||||
comment: approvalNotes.value
|
||||
})
|
||||
|
||||
ElMessage.success(message)
|
||||
approvalDialogVisible.value = false
|
||||
approvalNotes.value = ''
|
||||
instance_id.value = ''
|
||||
// 刷新列表
|
||||
getList()
|
||||
} catch (error) {
|
||||
console.error('审批失败:', error)
|
||||
ElMessage.error('审批失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.stocking-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.stocking-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// 审批弹出框样式
|
||||
.approval-dialog {
|
||||
.approval-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
color: #333333;
|
||||
.mr40 { margin-right: 40px; }
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title-bar {
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: #409eff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-items {
|
||||
.order-item {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #fafafa;
|
||||
|
||||
.item-header {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
|
||||
.item-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.item-details {
|
||||
.detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
label {
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.summary-item {
|
||||
.summary-label {
|
||||
font-weight: 600;
|
||||
color: #606266;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.summary-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.approval-notes {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 500px;
|
||||
margin: auto;
|
||||
gap: 12px;
|
||||
|
||||
.reject-btn {
|
||||
border-color: #409eff;
|
||||
color: #409eff;
|
||||
background: white;
|
||||
|
||||
&:hover {
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
|
||||
.approve-btn {
|
||||
background: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,495 @@
|
||||
<template>
|
||||
<div class="customer-page">
|
||||
<div class="customer-toolbar boxshadow">
|
||||
<div class="customer-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||
</el-form-item>
|
||||
</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"
|
||||
lass="customer-table"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column prop="contacts" label="客户名称" align="center" />
|
||||
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
||||
<el-table-column prop="channel_name" label="渠道" align="center" />
|
||||
<el-table-column prop="country_name" label="国家" align="center" />
|
||||
<!-- <el-table-column prop="phone" label="电话" align="center" />
|
||||
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
|
||||
<el-table-column prop="email" label="邮箱" align="center" /> -->
|
||||
<el-table-column prop="follow" label="联系方式" width="240px" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<div
|
||||
>WhatsApp:{{
|
||||
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
|
||||
}}</div
|
||||
>
|
||||
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
|
||||
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="grade_txt" label="等级" align="center" />
|
||||
<el-table-column prop="follow" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<span :class="statusClass(scope.row.follow)">
|
||||
{{ scope.row.follow_txt }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="user_name" label="标记人"></el-table-column>
|
||||
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||
<el-table-column
|
||||
prop="code"
|
||||
label="编号"
|
||||
align="center"
|
||||
min-width="120px"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||
<template #default="scope">
|
||||
<el-link
|
||||
type="success"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="onDetail(scope.row)"
|
||||
v-if="hasAuth('crm_customer@show')"
|
||||
>
|
||||
<i class="iconfont-sys"> </i>详情
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="goaudit(scope.row)"
|
||||
v-if="hasAuth('customer_audit@audet')"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 审核
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="detailDialogVisible" title="详情">
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="audetDialogVisible" title="审核" width="700px">
|
||||
<!-- 客户标记无效 原因与附件回显 -->
|
||||
<div style="margin-bottom: 16px">
|
||||
<div style="margin-bottom: 8px">
|
||||
<span>标记原因:</span>
|
||||
<span>{{ reportRemark || '-' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>附件说明:</span>
|
||||
<div
|
||||
v-if="reportAttachments.length"
|
||||
style="margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px"
|
||||
>
|
||||
<div
|
||||
v-for="(att, idx) in reportAttachments"
|
||||
:key="idx"
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid #f0f0f0;
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
"
|
||||
>
|
||||
<template v-if="/\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i.test(att.url)">
|
||||
<img
|
||||
:src="att.url"
|
||||
:alt="att.name"
|
||||
style="
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
"
|
||||
@click="openImagePreview(att.url)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="iconfont-sys"></i>
|
||||
</template>
|
||||
<el-link
|
||||
v-if="!/\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i.test(att.url)"
|
||||
type="primary"
|
||||
:underline="false"
|
||||
@click="downloadAttachment(att)"
|
||||
>下载</el-link
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无附件" style="--el-empty-padding: 10px 0" />
|
||||
</div>
|
||||
</div>
|
||||
<el-form :model="audetForm" label-width="50px">
|
||||
<el-form-item label="操作">
|
||||
<el-select v-model="audetForm.scene" filterable placeholder="请选择" style="width: 100%">
|
||||
<el-option label="彻底删除" value="delete" />
|
||||
<el-option label="恢复至公海" value="report" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<div style="display: flex; justify-content: flex-end; margin-top: 24px">
|
||||
<el-button @click="audetDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleAudetSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
<!-- 图片预览 -->
|
||||
<el-image-viewer
|
||||
v-if="imageViewerVisible"
|
||||
:url-list="imageViewerUrls"
|
||||
:initial-index="imageViewerIndex"
|
||||
@close="imageViewerVisible = false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
defineOptions({ name: 'AdminCustomerAudit' })
|
||||
const { hasAuth } = useAuth()
|
||||
const keyword = ref<string>('')
|
||||
const loading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const audetDialogVisible = ref(false)
|
||||
const detailRow = ref<any>(null)
|
||||
// 回显的无效原因与附件
|
||||
const reportRemark = ref<string>('')
|
||||
const reportAttachments = ref<Array<{ name: string; url: string }>>([])
|
||||
const imageViewerVisible = ref(false)
|
||||
const imageViewerUrls = ref<string[]>([])
|
||||
const imageViewerIndex = ref(0)
|
||||
// 规范化 URL:去除前缀 @,清理空白
|
||||
const normalizeUrl = (url: string): string => {
|
||||
return (url || '').trim().replace(/^@+/, '')
|
||||
}
|
||||
// 从 URL 提取文件名(忽略查询参数)
|
||||
const fileNameFromUrl = (url: string): string => {
|
||||
const u = normalizeUrl(url)
|
||||
try {
|
||||
const parsed = new URL(u)
|
||||
const name = parsed.pathname.split('/').pop() || ''
|
||||
return decodeURIComponent(name)
|
||||
} catch (_) {
|
||||
const path = u.split('?')[0]
|
||||
const name = path.split('/').pop() || u
|
||||
try {
|
||||
return decodeURIComponent(name)
|
||||
} catch {
|
||||
return name
|
||||
}
|
||||
}
|
||||
}
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const audetForm = reactive({
|
||||
id: '',
|
||||
scene: ''
|
||||
})
|
||||
const userOptions = ref<any>()
|
||||
const getuserList = async () => {
|
||||
// 这里调用获取用户列表的接口
|
||||
const res = await manageService.presetusers()
|
||||
userOptions.value = res || []
|
||||
// return res || []
|
||||
}
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
onMounted(() => {
|
||||
getuserList()
|
||||
gettableList()
|
||||
})
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
gettableList()
|
||||
})
|
||||
const gettableList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params: any = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
scene: 'audit'
|
||||
}
|
||||
if (keyword.value) {
|
||||
params.keyword = keyword.value
|
||||
}
|
||||
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
|
||||
tableData.value = data.map((item: any) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
gettableList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
gettableList()
|
||||
}
|
||||
function statusClass(status: any) {
|
||||
if (status === 0) return 'status-following'
|
||||
if (status === 1) return 'status-dealed'
|
||||
if (status === 2) return 'status-lost'
|
||||
return ''
|
||||
}
|
||||
//审核
|
||||
const goaudit = async (row: any) => {
|
||||
audetForm.id = row.id
|
||||
// 保存行数据用于展示
|
||||
detailRow.value = row
|
||||
// 兼容字段:优先 remark,其次 report_desc/desc
|
||||
reportRemark.value = row.audit?.remark || ''
|
||||
// 映射为可用的 {name, url}
|
||||
reportAttachments.value = (row.audit?.img || [])
|
||||
.map((it: any) => {
|
||||
if (typeof it === 'string') {
|
||||
const url = normalizeUrl(it)
|
||||
return { name: fileNameFromUrl(url) || url, url }
|
||||
}
|
||||
const url = normalizeUrl(it.storage_path_full || it.url || it.file_url || '')
|
||||
const name = it.original_name || it.name || it.file_name || fileNameFromUrl(url) || ''
|
||||
return { name, url }
|
||||
})
|
||||
.filter((x: { url: any }) => x.url)
|
||||
// 仅图片列表用于预览
|
||||
imageViewerUrls.value = reportAttachments.value
|
||||
.map((x) => normalizeUrl(x.url))
|
||||
.filter((u) => /\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i.test(u))
|
||||
audetDialogVisible.value = true
|
||||
}
|
||||
|
||||
const openImagePreview = (url: string) => {
|
||||
const nurl = normalizeUrl(url)
|
||||
// 按当前附件重建预览列表(确保与点击地址一致且包含查询参数的图片)
|
||||
const imgs = reportAttachments.value
|
||||
.map((x) => normalizeUrl(x.url))
|
||||
.filter((u) => /\.(jpg|jpeg|png|gif|webp|bmp)(\?.*)?$/i.test(u))
|
||||
imageViewerUrls.value = imgs.length > 0 ? imgs : [nurl]
|
||||
const idx = imageViewerUrls.value.findIndex((u) => u === nurl)
|
||||
imageViewerIndex.value = idx >= 0 ? idx : 0
|
||||
imageViewerVisible.value = true
|
||||
}
|
||||
|
||||
const downloadAttachment = (att: { name: string; url: string }) => {
|
||||
try {
|
||||
const a = document.createElement('a')
|
||||
a.href = att.url
|
||||
a.download = att.name || '附件'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
} catch (e) {
|
||||
console.error('下载失败', e)
|
||||
}
|
||||
}
|
||||
const handleAudetSubmit = async () => {
|
||||
const res = await manageService.crmCustomerAudit(audetForm)
|
||||
ElMessage.success(res?.message || '设置成功!')
|
||||
audetDialogVisible.value = false
|
||||
gettableList()
|
||||
}
|
||||
function processImages(html: string) {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, 'text/html')
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
function onDetail(row: any) {
|
||||
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||
detailRow.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-badge) {
|
||||
--el-badge-size: 16px;
|
||||
}
|
||||
.itemtag {
|
||||
margin-top: -16px;
|
||||
margin-left: -13px;
|
||||
}
|
||||
:deep(.el-table__header th) {
|
||||
background: #f7f9fa !important;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 450px !important;
|
||||
}
|
||||
:deep(.el-table__body tr.custom-table-row td) {
|
||||
border-bottom: 1px solid #f7f9fa !important;
|
||||
border-right: 1px solid #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr td:last-child) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:last-child td) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th) {
|
||||
border-bottom: 1px solid #f7f9fa !important;
|
||||
border-right: 1px solid #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th:last-child) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
border: 1px solid #f7f9fa !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.customer-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.customer-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.customer-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.customer-tags {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
width: 120px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.customer-tabs {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-following {
|
||||
color: #00a43f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-dealed {
|
||||
color: #3471ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-lost {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.customer-table {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.filter-more-btn {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.tag-items {
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
transition: background 0.2s;
|
||||
min-width: 100px;
|
||||
display: inline-block;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.tag-item {
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.tag-itemall {
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.tag-active {
|
||||
background: #3471ff;
|
||||
color: #fff;
|
||||
}
|
||||
.tag-active :deep(.el-badge__content) {
|
||||
color: #3471ff !important; /* 文字颜色 */
|
||||
background-color: #fff !important; /* 背景色 */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,673 @@
|
||||
<template>
|
||||
<div class="customer-page">
|
||||
<div class="customer-toolbar boxshadow">
|
||||
<div class="customer-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="plzyDialogVisible = true"
|
||||
v-if="hasAuth('highseas@allocation')"
|
||||
:disabled="selectedRows.length === 0"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 批量分配
|
||||
</el-button>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
lass="customer-table"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column type="selection" width="55px" align="center"></el-table-column>
|
||||
<el-table-column prop="contacts" label="客户名称" align="center" />
|
||||
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
||||
<el-table-column prop="channel_name" label="渠道" align="center" />
|
||||
<el-table-column prop="country_name" label="国家" align="center" />
|
||||
<!-- <el-table-column prop="phone" label="电话" align="center" />
|
||||
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
|
||||
<el-table-column prop="email" label="邮箱" align="center" /> -->
|
||||
<el-table-column prop="follow" label="联系方式" width="240px" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<div
|
||||
>WhatsApp:{{
|
||||
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
|
||||
}}</div
|
||||
>
|
||||
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
|
||||
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="grade_txt" label="等级" align="center" />
|
||||
<el-table-column prop="follow" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<span :class="statusClass(scope.row.follow)">
|
||||
{{ scope.row.follow_txt }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||
<el-table-column
|
||||
prop="code"
|
||||
label="编号"
|
||||
align="center"
|
||||
min-width="120px"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<!-- <el-table-column prop="follow_next" label="下次跟进"></el-table-column> -->
|
||||
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||
<template #default="scope">
|
||||
<el-link
|
||||
type="success"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="onDetail(scope.row)"
|
||||
v-if="hasAuth('highseas@detail')"
|
||||
>
|
||||
<i class="iconfont-sys"> </i>详情
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="gotransfer(scope.row)"
|
||||
v-if="hasAuth('highseas@allocation')"
|
||||
>
|
||||
<i class="iconfont-sys"> </i>分配
|
||||
</el-link>
|
||||
<el-link
|
||||
type="warning"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="gofollowdetail(scope.row)"
|
||||
v-if="hasAuth('highseas@follow_detail')"
|
||||
>
|
||||
<i class="iconfont-sys"> </i>跟进记录
|
||||
</el-link>
|
||||
<el-link
|
||||
type="danger"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="onDelete(scope.row)"
|
||||
v-if="hasAuth('highseas@delete')"
|
||||
>
|
||||
<i class="iconfont-sys"></i>删除
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog v-model="detailDialogVisible" title="详情" width="800px">
|
||||
<div class="detail-info-table">
|
||||
<div class="row">
|
||||
<div>客户名称:{{ detailRow.contacts }}</div>
|
||||
<div>电话:{{ detailRow.phone }}</div>
|
||||
<div>国家:{{ detailRow.country_name }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>渠道:{{ detailRow.channel_name }}</div>
|
||||
<div>邮箱:{{ detailRow.email }}</div>
|
||||
<div>WhatsApp:{{ detailRow.WhatsApp }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>发布人:{{ detailRow.enter_user_name }}</div>
|
||||
<div>录单时间:{{ detailRow.created_at }}</div>
|
||||
<div>
|
||||
等级:
|
||||
<span>
|
||||
{{ detailRow.magnitude_txt }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
状态:<span>{{ detailRow.grade_txt }}</span>
|
||||
</div>
|
||||
<div>
|
||||
跟进状态:<span>{{ detailRow.follow_txt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dtitle">详情内容</div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||
</el-dialog>
|
||||
<!-- 分配跟单对话框 -->
|
||||
<el-dialog v-model="zyDialogVisible" title="分配" width="360px">
|
||||
<el-form :model="zyForm" label-width="0">
|
||||
<el-form-item>
|
||||
<el-select
|
||||
v-model="zyForm.user_id"
|
||||
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>
|
||||
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||
<el-button @click="zyDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handlezySubmit">确定</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
<!-- 批量分配对话框 -->
|
||||
<el-dialog v-model="plzyDialogVisible" title="批量分配" width="360px">
|
||||
<el-form :model="plzyForm" label-width="0">
|
||||
<el-form-item>
|
||||
<el-select
|
||||
v-model="plzyForm.user_id"
|
||||
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>
|
||||
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||
<el-button @click="plzyDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleplzySubmit">确定</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 跟进记录对话框 -->
|
||||
<el-dialog v-model="followRecordDialogVisible" title="跟进记录" width="800px">
|
||||
<!-- 跟进记录列表 -->
|
||||
<div class="follow-record-list">
|
||||
<ElScrollbar>
|
||||
<template v-if="followRecords.length > 0">
|
||||
<ElTimeline>
|
||||
<ElTimelineItem
|
||||
v-for="item in followRecords"
|
||||
:key="item.time"
|
||||
:placement="TIMELINE_PLACEMENT"
|
||||
:color="activecolor"
|
||||
:center="true"
|
||||
>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-content-row">
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-text"
|
||||
>{{ item.type_txt }}
|
||||
<span class="timeline-mom">{{ item.created_at }}</span></p
|
||||
>
|
||||
<p class="timeline-mom"
|
||||
><span v-if="item.type == 1">跟进描述</span><span v-else>拜访内容</span>
|
||||
{{ item.audio_text }}</p
|
||||
>
|
||||
</div>
|
||||
<img
|
||||
v-if="item.picture_full"
|
||||
:src="item.picture_full"
|
||||
class="timeline-img"
|
||||
alt="图片"
|
||||
@click="handleImageClick(item.picture_full)"
|
||||
style="cursor: pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElTimelineItem>
|
||||
</ElTimeline>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 使用 Element Plus 的空状态组件 -->
|
||||
<el-empty :image="emptyimg" description="暂无跟进记录" />
|
||||
</template>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 图片预览对话框 -->
|
||||
<el-image-viewer
|
||||
v-if="imageViewerVisible"
|
||||
:url-list="imageList"
|
||||
:initial-index="imageViewerIndex"
|
||||
@close="closeImageViewer"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import emptyimg from '@/assets/img/crm/empty.png'
|
||||
defineOptions({ name: 'AdminHighseas' })
|
||||
const TIMELINE_PLACEMENT = 'top'
|
||||
const activecolor = '#3471ff'
|
||||
const { hasAuth } = useAuth()
|
||||
const loading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const zyDialogVisible = ref(false)
|
||||
const plzyDialogVisible = ref(false)
|
||||
const followRecordDialogVisible = ref(false)
|
||||
const detailRow = ref<any>(null)
|
||||
const currentCustomer = ref<any>(null)
|
||||
const followRecords = ref<any[]>([])
|
||||
const keyword = ref<string>('')
|
||||
// 图片预览相关
|
||||
const imageViewerVisible = ref(false)
|
||||
const imageList = ref<string[]>([])
|
||||
const imageViewerIndex = ref(0)
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const plzyForm = reactive({
|
||||
customer_id: '',
|
||||
user_id: ''
|
||||
})
|
||||
const zyForm = reactive({
|
||||
customer_id: '',
|
||||
user_id: ''
|
||||
})
|
||||
const userOptions = ref<any>()
|
||||
const getuserList = async () => {
|
||||
// 这里调用获取用户列表的接口
|
||||
const res = await manageService.presetusers()
|
||||
userOptions.value = res || []
|
||||
// return res || []
|
||||
}
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
onMounted(() => {
|
||||
getuserList()
|
||||
gettableList()
|
||||
})
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
gettableList()
|
||||
})
|
||||
const gettableList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params: any = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
scene: 'seas'
|
||||
}
|
||||
if (keyword.value) {
|
||||
params.keyword = keyword.value
|
||||
}
|
||||
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
|
||||
tableData.value = data.map((item: any) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
gettableList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
gettableList()
|
||||
}
|
||||
function statusClass(status: any) {
|
||||
if (status === 0) return 'status-following'
|
||||
if (status === 1) return 'status-dealed'
|
||||
if (status === 2) return 'status-lost'
|
||||
return ''
|
||||
}
|
||||
function processImages(html: string) {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, 'text/html')
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
function onDetail(row: any) {
|
||||
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||
detailRow.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
const gotransfer = (row: any) => {
|
||||
zyForm.customer_id = row.id
|
||||
zyDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 跟进记录
|
||||
const gofollowdetail = async (row: any) => {
|
||||
currentCustomer.value = row
|
||||
followRecordDialogVisible.value = true
|
||||
await manageService.crmCustomerFollow({ id: row.id }).then((res) => {
|
||||
followRecords.value = res
|
||||
})
|
||||
}
|
||||
|
||||
// 图片预览相关函数
|
||||
const handleImageClick = (url: string) => {
|
||||
imageList.value = [url]
|
||||
imageViewerIndex.value = 0
|
||||
imageViewerVisible.value = true
|
||||
}
|
||||
|
||||
const closeImageViewer = () => {
|
||||
imageViewerVisible.value = false
|
||||
imageList.value = []
|
||||
imageViewerIndex.value = 0
|
||||
}
|
||||
// 分配
|
||||
const handlezySubmit = async () => {
|
||||
const res = await manageService.crmCustomerAllocation({
|
||||
customer_id: zyForm.customer_id,
|
||||
user_id: zyForm.user_id
|
||||
})
|
||||
ElMessage.success(res?.message || '分配成功!')
|
||||
zyDialogVisible.value = false
|
||||
gettableList()
|
||||
}
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
// 批量分配
|
||||
const handleplzySubmit = async () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
plzyForm.customer_id = selectedRows.value.map((item) => item.id).join(',')
|
||||
const res = await manageService.crmCustomerAllocation(plzyForm)
|
||||
ElMessage.success(res?.message || '批量分配成功!')
|
||||
plzyDialogVisible.value = false
|
||||
gettableList()
|
||||
}
|
||||
//删除
|
||||
const onDelete = async (row: any) => {
|
||||
try {
|
||||
const res = await manageService.crmCustomerDelete({ customer_id: row.id })
|
||||
ElMessage.success(res?.message || '删除成功!')
|
||||
gettableList()
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败,请稍后再试')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-badge) {
|
||||
--el-badge-size: 16px;
|
||||
}
|
||||
.itemtag {
|
||||
margin-top: -16px;
|
||||
margin-left: -13px;
|
||||
}
|
||||
:deep(.el-table__header th) {
|
||||
background: #f7f9fa !important;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 450px !important;
|
||||
}
|
||||
:deep(.el-table__body tr.custom-table-row td) {
|
||||
border-bottom: 1px solid #f7f9fa !important;
|
||||
border-right: 1px solid #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr td:last-child) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:last-child td) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th) {
|
||||
border-bottom: 1px solid #f7f9fa !important;
|
||||
border-right: 1px solid #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th:last-child) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
border: 1px solid #f7f9fa !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.customer-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.customer-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.customer-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.customer-tags {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
width: 120px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.customer-tabs {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-following {
|
||||
color: #00a43f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-dealed {
|
||||
color: #3471ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-lost {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.customer-table {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.filter-more-btn {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.tag-items {
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
transition: background 0.2s;
|
||||
min-width: 100px;
|
||||
display: inline-block;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.tag-item {
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.tag-itemall {
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.tag-active {
|
||||
background: #3471ff;
|
||||
color: #fff;
|
||||
}
|
||||
.tag-active :deep(.el-badge__content) {
|
||||
color: #3471ff !important; /* 文字颜色 */
|
||||
background-color: #fff !important; /* 背景色 */
|
||||
}
|
||||
.detail-info-table {
|
||||
padding-left: 15px;
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 6px;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dtitle {
|
||||
line-height: 40px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.detils-content {
|
||||
padding-left: 15px;
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
.fenpei {
|
||||
color: #3471ff !important;
|
||||
}
|
||||
|
||||
.detil {
|
||||
color: #00a43f !important;
|
||||
}
|
||||
|
||||
.edit {
|
||||
color: #008477 !important;
|
||||
}
|
||||
|
||||
.delete {
|
||||
color: #f00 !important;
|
||||
}
|
||||
|
||||
.timeline-img {
|
||||
width: 180px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.follow-record-list {
|
||||
padding: 15px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
|
||||
.timeline-item {
|
||||
.timeline-content-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
|
||||
.timeline-text {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.timeline-mom {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-mom {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,897 @@
|
||||
<template>
|
||||
<div class="customer-page">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="customer-toolbar boxshadow">
|
||||
<div class="customer-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||
</el-form-item>
|
||||
<el-form-item label="跟单人" label-width="70px">
|
||||
<el-select
|
||||
v-model="selectValue"
|
||||
style="min-width: 120px"
|
||||
filterable
|
||||
multiple
|
||||
collapse-tags
|
||||
placeholder="请选择跟单人"
|
||||
>
|
||||
<el-option v-for="v in userOptions" :key="v.id" :label="v.name" :value="v.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户渠道" prop="channel_id">
|
||||
<el-select v-model="selectchannel_id" filterable placeholder="请选择客户渠道">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-button type="primary" @click="exportfn" v-if="hasAuth('customer@export')">
|
||||
<i class="iconfont-sys"></i> 导出
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选标签 -->
|
||||
<div class="customer-tags boxshadow">
|
||||
<!-- 前3组 -->
|
||||
<div class="filter-row" v-for="item in visibleTagGroups" :key="item.id">
|
||||
<span class="filter-label">{{ item.name }}</span>
|
||||
<div class="filter-tags">
|
||||
<span
|
||||
:class="['tag-itemall', { 'tag-active': tagActiveMap[item.id] === '' }]"
|
||||
@click="handleTagClick(item.id, '')"
|
||||
>全部</span
|
||||
>
|
||||
<span
|
||||
class="tag-items"
|
||||
v-for="item1 in item.tags"
|
||||
:key="item1.id"
|
||||
@click="handleTagClick(item.id, item1.id)"
|
||||
>
|
||||
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
|
||||
{{ item1.name }}
|
||||
</span>
|
||||
<!-- <el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge> -->
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 更多 -->
|
||||
<transition name="fade">
|
||||
<div v-if="showMore">
|
||||
<div class="filter-row" v-for="item in moreTagGroups" :key="item.id">
|
||||
<span class="filter-label">{{ item.name }}</span>
|
||||
<div class="filter-tags">
|
||||
<span
|
||||
:class="['tag-itemall', { 'tag-active': tagActiveMap[item.id] === '' }]"
|
||||
@click="handleTagClick(item.id, '')"
|
||||
>全部</span
|
||||
>
|
||||
<span
|
||||
class="tag-items"
|
||||
v-for="item1 in item.tags"
|
||||
:key="item1.id"
|
||||
@click="handleTagClick(item.id, item1.id)"
|
||||
>
|
||||
<span :class="['tag-item', { 'tag-active': tagActiveMap[item.id] === item1.id }]">
|
||||
{{ item1.name }}
|
||||
</span>
|
||||
<!-- <el-badge :value="item1.count" :max="99" class="itemtag" type="primary"> </el-badge> -->
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div class="filter-more-btn" v-if="moreTagGroups.length">
|
||||
<el-link underline="never" @click="showMore = !showMore">
|
||||
{{ showMore ? '收起' : '更多' }}
|
||||
<i v-if="showMore" class="iconfont-sys"> </i>
|
||||
<i v-else class="iconfont-sys"></i>
|
||||
</el-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="plzyDialogVisible = true"
|
||||
v-if="hasAuth('customer@transfer')"
|
||||
:disabled="selectedRows.length === 0"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 批量转移
|
||||
</el-button>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
lass="customer-table"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column type="selection" width="55px" align="center"></el-table-column>
|
||||
<el-table-column prop="contacts" label="客户名称" align="center" min-width="120px" />
|
||||
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
||||
<el-table-column prop="channel_name" label="渠道" align="center" />
|
||||
<el-table-column prop="country_name" label="国家" align="center" />
|
||||
<!-- <el-table-column prop="phone" label="电话" align="center" />
|
||||
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
|
||||
<el-table-column prop="email" label="邮箱" align="center" /> -->
|
||||
<el-table-column prop="follow" label="联系方式" width="240px" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<div
|
||||
>WhatsApp:{{
|
||||
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
|
||||
}}</div
|
||||
>
|
||||
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
|
||||
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="grade_txt" label="等级" align="center" />
|
||||
<el-table-column prop="follow" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<span :class="statusClass(scope.row.follow)">
|
||||
{{ scope.row.follow_txt }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||
<el-table-column prop="user_name" label="跟单人"></el-table-column>
|
||||
<el-table-column prop="follow_last" label="上次跟进"></el-table-column>
|
||||
<el-table-column prop="follow_next" label="下次跟进"></el-table-column>
|
||||
<el-table-column
|
||||
prop="code"
|
||||
label="编号"
|
||||
align="center"
|
||||
min-width="120px"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||
<template #default="scope">
|
||||
<el-link
|
||||
type="success"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="onDetail(scope.row)"
|
||||
v-if="hasAuth('customer@detail')"
|
||||
>
|
||||
<i class="iconfont-sys"> </i>详情
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="gotransfer(scope.row)"
|
||||
v-if="hasAuth('customer@transfer')"
|
||||
>
|
||||
<i class="iconfont-sys"> </i>转移
|
||||
</el-link>
|
||||
<el-link
|
||||
type="warning"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="gofollowdetail(scope.row)"
|
||||
v-if="hasAuth('customer@follow_detail')"
|
||||
>
|
||||
<i class="iconfont-sys"> </i>跟进记录
|
||||
</el-link>
|
||||
<el-link
|
||||
type="danger"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="delectfn(scope.row)"
|
||||
v-if="hasAuth('customer@delete')"
|
||||
>
|
||||
<i class="iconfont-sys"></i>删除
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog v-model="detailDialogVisible" title="详情" width="800px">
|
||||
<div class="detail-info-table">
|
||||
<div class="row">
|
||||
<div>客户名称:{{ detailRow.contacts }}</div>
|
||||
<div>电话:{{ detailRow.phone }}</div>
|
||||
<div>国家:{{ detailRow.country_name }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>渠道:{{ detailRow.channel_name }}</div>
|
||||
<div>邮箱:{{ detailRow.email }}</div>
|
||||
<div>WhatsApp:{{ detailRow.WhatsApp }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>发布人:{{ detailRow.enter_user_name }}</div>
|
||||
<div>跟单人:{{ detailRow.user_name }}</div>
|
||||
<div>录单时间:{{ detailRow.created_at }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
等级:
|
||||
<span>
|
||||
{{ detailRow.magnitude_txt }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
状态:<span>{{ detailRow.grade_txt }}</span>
|
||||
</div>
|
||||
<div>
|
||||
跟进状态:<span>{{ detailRow.follow_txt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>备注:{{ detailRow.describe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="detailRow.desc_collect != ''" class="dtitle">详情内容</div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||
</el-dialog>
|
||||
<!-- 转移跟单对话框 -->
|
||||
<el-dialog v-model="zyDialogVisible" title="转移" width="360px">
|
||||
<el-form :model="zyForm" label-width="0">
|
||||
<el-form-item>
|
||||
<el-select
|
||||
v-model="zyForm.user_id"
|
||||
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>
|
||||
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||
<el-button @click="zyDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handlezySubmit">确定</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
<!-- 批量转移对话框 -->
|
||||
<el-dialog v-model="plzyDialogVisible" title="批量转移" width="360px">
|
||||
<el-form :model="plzyForm" label-width="0">
|
||||
<el-form-item>
|
||||
<el-select
|
||||
v-model="plzyForm.user_id"
|
||||
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>
|
||||
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||
<el-button @click="plzyDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleplzySubmit">确定</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
<!-- 跟进记录对话框 -->
|
||||
<el-dialog v-model="followRecordDialogVisible" title="跟进记录" width="800px">
|
||||
<!-- 跟进记录列表 -->
|
||||
<div class="follow-record-list">
|
||||
<ElScrollbar>
|
||||
<template v-if="followRecords.length > 0">
|
||||
<ElTimeline>
|
||||
<ElTimelineItem
|
||||
v-for="item in followRecords"
|
||||
:key="item.time"
|
||||
:placement="TIMELINE_PLACEMENT"
|
||||
:color="activecolor"
|
||||
:center="true"
|
||||
>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-content-row">
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-text"
|
||||
>{{ item.type_txt }}
|
||||
<span class="timeline-mom">{{ item.created_at }}</span></p
|
||||
>
|
||||
<p class="timeline-mom"
|
||||
><span v-if="item.type == 1">跟进描述</span><span v-else>拜访内容</span>
|
||||
{{ item.audio_text }}</p
|
||||
>
|
||||
</div>
|
||||
<img
|
||||
v-if="item.picture_full"
|
||||
:src="item.picture_full"
|
||||
class="timeline-img"
|
||||
alt="图片"
|
||||
@click="handleImageClick(item.picture_full)"
|
||||
style="cursor: pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElTimelineItem>
|
||||
</ElTimeline>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 使用 Element Plus 的空状态组件 -->
|
||||
<el-empty :image="emptyimg" description="暂无跟进记录" />
|
||||
<!-- <el-empty /> -->
|
||||
<!-- 或者自定义图片 -->
|
||||
<!-- <img src="你的空图片地址.png" alt="暂无数据" class="empty-img" /> -->
|
||||
</template>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 图片预览对话框 -->
|
||||
<el-image-viewer
|
||||
v-if="imageViewerVisible"
|
||||
:url-list="imageList"
|
||||
:initial-index="imageViewerIndex"
|
||||
@close="closeImageViewer"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue'
|
||||
import { CrmService } from '@/api/crmApi'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import emptyimg from '@/assets/img/crm/empty.png'
|
||||
defineOptions({ name: 'AdminCustomer' })
|
||||
const TIMELINE_PLACEMENT = 'top'
|
||||
const activecolor = '#3471ff'
|
||||
const { hasAuth } = useAuth()
|
||||
const selectValue = ref<string[]>([])
|
||||
const selectchannel_id = ref<string>('')
|
||||
const showMore = ref(false)
|
||||
const loading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const zyDialogVisible = ref(false)
|
||||
const plzyDialogVisible = ref(false)
|
||||
const followRecordDialogVisible = ref(false)
|
||||
const detailRow = ref<any>(null)
|
||||
const currentCustomer = ref<any>(null)
|
||||
const followRecords = ref<any[]>([])
|
||||
const keyword = ref<string>('')
|
||||
// 图片预览相关
|
||||
const imageViewerVisible = ref(false)
|
||||
const imageList = ref<string[]>([])
|
||||
const imageViewerIndex = ref(0)
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const plzyForm = reactive({
|
||||
customer_id: '',
|
||||
user_id: ''
|
||||
})
|
||||
const zyForm = reactive({
|
||||
customer_id: '',
|
||||
user_id: ''
|
||||
})
|
||||
const channelOptions = ref<any>()
|
||||
//渠道
|
||||
const getgetChannel = async () => {
|
||||
const res = await CrmService.getChannel()
|
||||
channelOptions.value = res || []
|
||||
}
|
||||
const userOptions = ref<any>()
|
||||
const getuserList = async () => {
|
||||
// 这里调用获取用户列表的接口
|
||||
const res = await manageService.presetusers()
|
||||
userOptions.value = res || []
|
||||
// return res || []
|
||||
}
|
||||
// 获取列表数据
|
||||
const tagActiveMap = reactive<{ [groupId: string]: string }>({})
|
||||
//获取标签
|
||||
type TagGroup = { id: string; name: string; tags: { id: string; name: string; count: number }[] }
|
||||
const tagGroups = ref<TagGroup[]>([])
|
||||
const tagGrouplistfn = async () => {
|
||||
const res = await CrmService.tagGrouplist({ scene: 'customers' })
|
||||
tagGroups.value = res || []
|
||||
tagGroups.value.forEach((group) => {
|
||||
if (group.tags && Array.isArray(group.tags)) {
|
||||
tagActiveMap[group.id] = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
// 计算前3组和更多组
|
||||
// eslint-disable-next-line vue/valid-define-props
|
||||
defineProps()
|
||||
const visibleTagGroups = computed(() => tagGroups.value.slice(0, 3))
|
||||
const moreTagGroups = computed(() => tagGroups.value.slice(3))
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
onMounted(() => {
|
||||
getgetChannel()
|
||||
getuserList()
|
||||
gettableList()
|
||||
tagGrouplistfn()
|
||||
})
|
||||
// tabs 切换联动
|
||||
watch([selectValue, keyword, selectchannel_id], () => {
|
||||
pagination.currentPage = 1
|
||||
gettableList()
|
||||
})
|
||||
const gettableList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params: any = {
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
}
|
||||
if (selectValue.value && selectValue.value.length) {
|
||||
params.user_id = selectValue.value.join(',')
|
||||
}
|
||||
if (keyword.value) {
|
||||
params.keyword = keyword.value
|
||||
}
|
||||
if (selectchannel_id.value) {
|
||||
params.channel_id = selectchannel_id.value
|
||||
}
|
||||
// 标签参数,所有选中的标签id组成数组
|
||||
const selectedTagIds = Object.values(tagActiveMap).filter(Boolean)
|
||||
if (selectedTagIds.length) {
|
||||
params.tag_id = selectedTagIds
|
||||
}
|
||||
console.log(params)
|
||||
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
|
||||
tableData.value = data.map((item: any) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
gettableList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
gettableList()
|
||||
}
|
||||
function statusClass(status: any) {
|
||||
if (status === 0) return 'status-following'
|
||||
if (status === 1) return 'status-dealed'
|
||||
if (status === 2) return 'status-lost'
|
||||
return ''
|
||||
}
|
||||
//导出
|
||||
const exportfn = () => {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params: any = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
user_id: selectValue.value.join(','),
|
||||
tag_id: Object.values(tagActiveMap).filter(Boolean),
|
||||
keyword: keyword.value,
|
||||
channel_id: selectchannel_id.value
|
||||
}
|
||||
// manageService.crmCustomerExport(params)
|
||||
try {
|
||||
// 1. 调用接口,获取blob
|
||||
// const res = await manageService.crmCustomerExport(params)
|
||||
const urls =
|
||||
import.meta.env.VITE_API_URL +
|
||||
`/admin/crm/customer/export?current=${params.current}&size=${params.size}&user_id=${params.user_id}&tag_id=${params.tag_id}&channel_id=${params.channel_id}&keyword=${params.keyword}`
|
||||
// 2. 创建blob对象
|
||||
// const blob = new Blob([res], { type: 'application/vnd.ms-excel' }) // 或根据后端实际类型调整
|
||||
// 3. 创建下载链接
|
||||
const url = urls
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = '客户数据.xlsx' // 可自定义文件名
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (e) {
|
||||
ElMessage.error('导出失败')
|
||||
}
|
||||
}
|
||||
// 标签选中状态,key为组id,值为选中的标签id
|
||||
function handleTagClick(groupId: string, tagId: string) {
|
||||
// if (tagActiveMap[groupId] === tagId) {
|
||||
// tagActiveMap[groupId] = '' // 取消选中
|
||||
// } else {
|
||||
tagActiveMap[groupId] = tagId // 选中
|
||||
// }
|
||||
pagination.currentPage = 1
|
||||
gettableList()
|
||||
}
|
||||
function processImages(html: string) {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, 'text/html')
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
function onDetail(row: any) {
|
||||
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||
detailRow.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
const gotransfer = (row: any) => {
|
||||
zyForm.customer_id = row.id
|
||||
zyDialogVisible.value = true
|
||||
}
|
||||
// 跟进记录
|
||||
const gofollowdetail = async (row: any) => {
|
||||
currentCustomer.value = row
|
||||
followRecordDialogVisible.value = true
|
||||
await manageService.crmCustomerFollow({ id: row.id }).then((res) => {
|
||||
followRecords.value = res
|
||||
})
|
||||
}
|
||||
|
||||
// 图片预览相关函数
|
||||
const handleImageClick = (url: string) => {
|
||||
imageList.value = [url]
|
||||
imageViewerIndex.value = 0
|
||||
imageViewerVisible.value = true
|
||||
}
|
||||
|
||||
const closeImageViewer = () => {
|
||||
imageViewerVisible.value = false
|
||||
imageList.value = []
|
||||
imageViewerIndex.value = 0
|
||||
}
|
||||
// 转移
|
||||
const handlezySubmit = async () => {
|
||||
const res = await manageService.crmCustomerTransfer({
|
||||
customer_id: zyForm.customer_id,
|
||||
user_id: zyForm.user_id
|
||||
})
|
||||
ElMessage.success(res?.message || '转移成功!')
|
||||
zyDialogVisible.value = false
|
||||
gettableList()
|
||||
}
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
// 批量转移
|
||||
const handleplzySubmit = async () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请至少选择一条记录')
|
||||
return
|
||||
}
|
||||
plzyForm.customer_id = selectedRows.value.map((item) => item.id).join(',')
|
||||
const res = await manageService.crmCustomerTransfer(plzyForm)
|
||||
ElMessage.success(res?.message || '批量转移成功!')
|
||||
plzyDialogVisible.value = false
|
||||
gettableList()
|
||||
}
|
||||
const delectfn = (row: any) => {
|
||||
ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'list' })
|
||||
ElMessage.success(res?.message || '提交成功!')
|
||||
gettableList()
|
||||
// 你可以在这里调用接口或做后续处理
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户点击了取消
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.timeline-img {
|
||||
width: 180px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
:deep(.el-badge) {
|
||||
--el-badge-size: 16px;
|
||||
}
|
||||
.itemtag {
|
||||
margin-top: -16px;
|
||||
margin-left: -13px;
|
||||
}
|
||||
:deep(.el-table__header th) {
|
||||
background: #f7f9fa !important;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 450px !important;
|
||||
}
|
||||
:deep(.el-table__body tr.custom-table-row td) {
|
||||
border-bottom: 1px solid #f7f9fa !important;
|
||||
border-right: 1px solid #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr td:last-child) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:last-child td) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th) {
|
||||
border-bottom: 1px solid #f7f9fa !important;
|
||||
border-right: 1px solid #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th:last-child) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
border: 1px solid #f7f9fa !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.customer-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.customer-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.customer-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.customer-tags {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
width: 120px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.customer-tabs {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-following {
|
||||
color: #00a43f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-dealed {
|
||||
color: #3471ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-lost {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.customer-table {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.filter-more-btn {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.tag-items {
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
transition: background 0.2s;
|
||||
min-width: 100px;
|
||||
display: inline-block;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.tag-item {
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.tag-itemall {
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.tag-active {
|
||||
background: #3471ff;
|
||||
color: #fff;
|
||||
}
|
||||
.tag-active :deep(.el-badge__content) {
|
||||
color: #3471ff !important; /* 文字颜色 */
|
||||
background-color: #fff !important; /* 背景色 */
|
||||
}
|
||||
.detail-info-table {
|
||||
padding-left: 15px;
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 6px;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dtitle {
|
||||
line-height: 40px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.detils-content {
|
||||
padding-left: 15px;
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.follow-record-header {
|
||||
padding: 15px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.customer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.follow-record-list {
|
||||
padding: 15px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
.list-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.record-type {
|
||||
font-size: 14px;
|
||||
color: #3471ff;
|
||||
font-weight: bold;
|
||||
background-color: #e6f7ff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.record-user {
|
||||
font-size: 14px;
|
||||
color: #3471ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.record-content {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.record-footer {
|
||||
.next-follow {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-records {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,574 @@
|
||||
<template>
|
||||
<div class="customer-page">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="customer-toolbar boxshadow">
|
||||
<div class="customer-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<!-- <el-button type="primary" @click="exportfn" v-if="hasAuth('customer@export')">
|
||||
<i class="iconfont-sys"></i> 导出
|
||||
</el-button> -->
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
lass="customer-table"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column type="selection" width="55px" align="center"></el-table-column>
|
||||
<el-table-column prop="contacts" label="客户名称" align="center" />
|
||||
<el-table-column prop="magnitude_txt" label="客户商机" align="center" />
|
||||
<el-table-column prop="channel_name" label="渠道" align="center" />
|
||||
<el-table-column prop="country_name" label="国家" align="center" />
|
||||
<!-- <el-table-column prop="phone" label="电话" align="center" />
|
||||
<el-table-column prop="WhatsApp" label="WhatsApp" align="center" />
|
||||
<el-table-column prop="email" label="邮箱" align="center" /> -->
|
||||
<el-table-column prop="follow" label="联系方式" width="240px" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<div
|
||||
>WhatsApp:{{
|
||||
scope.row.WhatsApp == '' ? ' --' : '【' + scope.row.WhatsApp + ' 】'
|
||||
}}</div
|
||||
>
|
||||
<div>电话:{{ scope.row.phone == '' ? ' --' : '【' + scope.row.phone + ' 】' }}</div>
|
||||
<div>邮箱:{{ scope.row.email == '' ? ' --' : '【' + scope.row.email + ' 】' }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="grade_txt" label="等级" align="center" />
|
||||
<!-- <el-table-column prop="follow" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<span :class="statusClass(scope.row.follow)">
|
||||
{{ scope.row.follow_txt }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="enter_user_name" label="录单人"></el-table-column>
|
||||
<el-table-column prop="user_name" label="跟单人"></el-table-column>
|
||||
<el-table-column prop="del_source" label="来源"></el-table-column>
|
||||
<el-table-column prop="follow_last" label="上次跟进"></el-table-column>
|
||||
<el-table-column prop="follow_next" label="下次跟进"></el-table-column>
|
||||
<el-table-column
|
||||
prop="code"
|
||||
label="编号"
|
||||
align="center"
|
||||
min-width="120px"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="200px" align="center">
|
||||
<template #default="scope">
|
||||
<el-link
|
||||
type="success"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="onDetail(scope.row)"
|
||||
v-if="hasAuth('recycle@detail')"
|
||||
>
|
||||
<i class="iconfont-sys"> </i>详情
|
||||
</el-link>
|
||||
<el-link
|
||||
type="primary"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="recoverfn(scope.row)"
|
||||
v-if="hasAuth('recycle@recover')"
|
||||
>
|
||||
<i class="iconfont-sys"> </i>恢复
|
||||
</el-link>
|
||||
<el-link
|
||||
type="danger"
|
||||
underline="never"
|
||||
style="margin-right: 8px"
|
||||
@click="delectfn(scope.row)"
|
||||
v-if="hasAuth('recycle@delete')"
|
||||
>
|
||||
<i class="iconfont-sys"></i>删除
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 详情对话框 -->
|
||||
<el-dialog v-model="detailDialogVisible" title="详情" width="800px">
|
||||
<div class="detail-info-table">
|
||||
<div class="row">
|
||||
<div>客户名称:{{ detailRow.contacts }}</div>
|
||||
<div>电话:{{ detailRow.phone }}</div>
|
||||
<div>国家:{{ detailRow.country_name }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>渠道:{{ detailRow.channel_name }}</div>
|
||||
<div>邮箱:{{ detailRow.email }}</div>
|
||||
<div>WhatsApp:{{ detailRow.WhatsApp }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>发布人:{{ detailRow.enter_user_name }}</div>
|
||||
<div>跟单人:{{ detailRow.user_name }}</div>
|
||||
<div>录单时间:{{ detailRow.created_at }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
等级:
|
||||
<span>
|
||||
{{ detailRow.magnitude_txt }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
状态:<span>{{ detailRow.grade_txt }}</span>
|
||||
</div>
|
||||
<div>
|
||||
跟进状态:<span>{{ detailRow.follow_txt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>备注:{{ detailRow.describe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="detailRow.desc_collect != ''" class="dtitle">详情内容</div>
|
||||
<div v-if="detailRow" v-html="detailRow.desc_collect" class="detils-content"></div>
|
||||
</el-dialog>
|
||||
<!-- 图片预览对话框 -->
|
||||
<el-image-viewer
|
||||
v-if="imageViewerVisible"
|
||||
:url-list="imageList"
|
||||
:initial-index="imageViewerIndex"
|
||||
@close="closeImageViewer"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
defineOptions({ name: 'AdminRecycle' })
|
||||
const { hasAuth } = useAuth()
|
||||
const selectValue = ref<string[]>([])
|
||||
const loading = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const zyDialogVisible = ref(false)
|
||||
const detailRow = ref<any>(null)
|
||||
const keyword = ref<string>('')
|
||||
// 图片预览相关
|
||||
const imageViewerVisible = ref(false)
|
||||
const imageList = ref<string[]>([])
|
||||
const imageViewerIndex = ref(0)
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const zyForm = reactive({
|
||||
customer_id: '',
|
||||
user_id: ''
|
||||
})
|
||||
const userOptions = ref<any>()
|
||||
const getuserList = async () => {
|
||||
// 这里调用获取用户列表的接口
|
||||
const res = await manageService.presetusers()
|
||||
userOptions.value = res || []
|
||||
// return res || []
|
||||
}
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
onMounted(() => {
|
||||
getuserList()
|
||||
gettableList()
|
||||
})
|
||||
// tabs 切换联动
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
gettableList()
|
||||
})
|
||||
const gettableList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params: any = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
scene: 'recyclea'
|
||||
}
|
||||
if (keyword.value) {
|
||||
params.keyword = keyword.value
|
||||
}
|
||||
const { data, current, size, total } = await manageService.getcrmCustomerList(params)
|
||||
tableData.value = data.map((item: any) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
gettableList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
gettableList()
|
||||
}
|
||||
//导出
|
||||
function processImages(html: string) {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, 'text/html')
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
function onDetail(row: any) {
|
||||
row.desc_collect = processImages(row.desc_collect) // 处理图片
|
||||
detailRow.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
const recoverfn = (row: any) => {
|
||||
ElMessageBox.confirm('确定要恢复此条数据吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await manageService.customerRestore({ id: row.id })
|
||||
ElMessage.success(res?.message || '提交成功!')
|
||||
gettableList()
|
||||
// 你可以在这里调用接口或做后续处理
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户点击了取消
|
||||
})
|
||||
}
|
||||
const delectfn = (row: any) => {
|
||||
ElMessageBox.confirm('确定要删除此客户吗?删除后将无法恢复!', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await manageService.crmCustomerDelete({ customer_id: row.id, scene: 'force' })
|
||||
ElMessage.success(res?.message || '提交成功!')
|
||||
gettableList()
|
||||
// 你可以在这里调用接口或做后续处理
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户点击了取消
|
||||
})
|
||||
}
|
||||
const closeImageViewer = () => {
|
||||
imageViewerVisible.value = false
|
||||
imageList.value = []
|
||||
imageViewerIndex.value = 0
|
||||
}
|
||||
// 选中的行数据
|
||||
const selectedRows = ref<any[]>([])
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.timeline-img {
|
||||
width: 180px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
:deep(.el-badge) {
|
||||
--el-badge-size: 16px;
|
||||
}
|
||||
.itemtag {
|
||||
margin-top: -16px;
|
||||
margin-left: -13px;
|
||||
}
|
||||
:deep(.el-table__header th) {
|
||||
background: #f7f9fa !important;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 450px !important;
|
||||
}
|
||||
:deep(.el-table__body tr.custom-table-row td) {
|
||||
border-bottom: 1px solid #f7f9fa !important;
|
||||
border-right: 1px solid #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr td:last-child) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:last-child td) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th) {
|
||||
border-bottom: 1px solid #f7f9fa !important;
|
||||
border-right: 1px solid #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th:last-child) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
border: 1px solid #f7f9fa !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.customer-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.customer-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.customer-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.customer-tags {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
width: 120px;
|
||||
color: #666;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-tags {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.customer-tabs {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.status-following {
|
||||
color: #00a43f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-dealed {
|
||||
color: #3471ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-lost {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.customer-table {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.filter-more-btn {
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.tag-items {
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
transition: background 0.2s;
|
||||
min-width: 100px;
|
||||
display: inline-block;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.tag-item {
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.tag-itemall {
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
.tag-active {
|
||||
background: #3471ff;
|
||||
color: #fff;
|
||||
}
|
||||
.tag-active :deep(.el-badge__content) {
|
||||
color: #3471ff !important; /* 文字颜色 */
|
||||
background-color: #fff !important; /* 背景色 */
|
||||
}
|
||||
.detail-info-table {
|
||||
padding-left: 15px;
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 6px;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dtitle {
|
||||
line-height: 40px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding-left: 15px;
|
||||
}
|
||||
.detils-content {
|
||||
padding-left: 15px;
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.follow-record-header {
|
||||
padding: 15px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.customer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.follow-record-list {
|
||||
padding: 15px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
.list-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 15px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
padding: 10px 15px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.record-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.record-type {
|
||||
font-size: 14px;
|
||||
color: #3471ff;
|
||||
font-weight: bold;
|
||||
background-color: #e6f7ff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.record-user {
|
||||
font-size: 14px;
|
||||
color: #3471ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.record-content {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.record-footer {
|
||||
.next-follow {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-records {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
padding: 20px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="account-page">
|
||||
<div class="account-toolbar boxshadow">
|
||||
<div class="account-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input
|
||||
style="min-width: 180px"
|
||||
v-model="keyword"
|
||||
placeholder="请输入关键字"
|
||||
clearable
|
||||
@keyup.enter="getAccountList"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getAccountList" v-ripple>搜索</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<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('account@add')" type="primary"
|
||||
>新增账户</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="short_desc" min-width="160" />
|
||||
<ElTableColumn label="账户名称" prop="account_name" min-width="160" />
|
||||
<ElTableColumn label="账户号码" prop="account_number" min-width="160" />
|
||||
<ElTableColumn label="银行名称" prop="bank_name" min-width="140" />
|
||||
<!-- <ElTableColumn label="开户支行" prop="bank_branch" min-width="160" /> -->
|
||||
<ElTableColumn label="公司名称" prop="corp_name" min-width="160" />
|
||||
<ElTableColumn label="货币" prop="currency_id" min-width="120">
|
||||
<template #default="scope">
|
||||
<span>{{ getCurrencyLabel(scope.row.currency_id) }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="排序" prop="sort" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<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="操作" min-width="160px" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('account@update')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteAccount(scope.row)"
|
||||
v-if="hasAuth('account@destroy')"
|
||||
>删除</el-button
|
||||
>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增账户' : '编辑账户'"
|
||||
align-center
|
||||
width="700px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="form">
|
||||
<ElFormItem label="账户简称" prop="short_desc">
|
||||
<ElInput v-model="form.short_desc" placeholder="请输入账户简称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账户名称" prop="account_name">
|
||||
<ElInput v-model="form.account_name" placeholder="请输入账户名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账户号码" prop="account_number">
|
||||
<ElInput v-model="form.account_number" placeholder="请输入账户号码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="银行名称" prop="bank_name">
|
||||
<ElInput v-model="form.bank_name" placeholder="请输入银行名称" />
|
||||
</ElFormItem>
|
||||
<!-- <ElFormItem label="开户支行" prop="bank_branch">
|
||||
<ElInput v-model="form.bank_branch" placeholder="请输入开户支行" />
|
||||
</ElFormItem> -->
|
||||
<ElFormItem label="公司名称" prop="corp_id">
|
||||
<el-select v-model="form.corp_id" placeholder="请选择公司" style="width: 100%">
|
||||
<el-option
|
||||
v-for="account in companyList"
|
||||
:key="account.id"
|
||||
:label="account.name"
|
||||
:value="account.id"
|
||||
/>
|
||||
</el-select>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账户货币类型" prop="currency_id">
|
||||
<el-select v-model="form.currency_id" placeholder="请选择货币" style="width: 100%">
|
||||
<el-option
|
||||
v-for="opt in currencyOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.currency_name + ' (' + opt.currency_code + ')'"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="form.sort" :min="0" :max="9999" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="账户描述" prop="description">
|
||||
<ElInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="300"
|
||||
show-word-limit
|
||||
placeholder="请输入账户描述"
|
||||
/>
|
||||
</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 { financeService } from '@/api/financeApi'
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'FinanceAccount' })
|
||||
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const keyword = ref<string>('')
|
||||
const currencyFilter = ref<number | undefined>(undefined)
|
||||
const statusFilter = ref<number | undefined>(undefined)
|
||||
|
||||
// 下拉:货币
|
||||
const currencyOptions = ref<any[]>([])
|
||||
const loadCurrencyOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCurrencySelect()
|
||||
currencyOptions.value = res || []
|
||||
} catch (e) {
|
||||
currencyOptions.value = []
|
||||
}
|
||||
}
|
||||
const companyList = ref<any>()
|
||||
const getcompanyList = async () => {
|
||||
const response = await UserService.getCompanyList()
|
||||
companyList.value = response || []
|
||||
}
|
||||
|
||||
const form = reactive<any>({
|
||||
id: '',
|
||||
currency_id: undefined,
|
||||
short_desc: '',
|
||||
status: 1,
|
||||
sort: 0,
|
||||
bank_name: '',
|
||||
bank_branch: '',
|
||||
description: '',
|
||||
account_number: '',
|
||||
account_name: '',
|
||||
corp_id: ''
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
short_desc: [{ required: true, message: '请输入账户简称', trigger: 'blur' }],
|
||||
account_name: [{ required: true, message: '请输入账户名称', trigger: 'blur' }],
|
||||
account_number: [{ required: true, message: '请输入账户号码', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const tableData = ref<any[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
getcompanyList()
|
||||
loadCurrencyOptions()
|
||||
getAccountList()
|
||||
})
|
||||
watch([keyword, currencyFilter, statusFilter], () => {
|
||||
pagination.currentPage = 1
|
||||
getAccountList()
|
||||
})
|
||||
|
||||
const getAccountList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
}
|
||||
const { records, current, size, total } = await financeService.getAccountList(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
|
||||
getAccountList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getAccountList()
|
||||
}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
short_desc: row.short_desc,
|
||||
currency_id: row.currency_id,
|
||||
status: row.status,
|
||||
sort: row.sort ?? 0,
|
||||
bank_name: row.bank_name,
|
||||
bank_branch: row.bank_branch,
|
||||
description: row.description,
|
||||
account_number: row.account_number,
|
||||
account_name: row.account_name,
|
||||
corp_id: row.corp_id
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
id: '',
|
||||
short_desc: '',
|
||||
currency_id: undefined,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
bank_name: '',
|
||||
bank_branch: '',
|
||||
description: '',
|
||||
account_number: '',
|
||||
account_name: '',
|
||||
corp_id: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAccount = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该账户吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const { message } = await financeService.accountDelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getAccountList()
|
||||
} 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 financeService.accountAdd({
|
||||
currency_id: form.currency_id,
|
||||
short_desc: form.short_desc,
|
||||
status: form.status,
|
||||
sort: form.sort,
|
||||
bank_name: form.bank_name,
|
||||
bank_branch: form.bank_branch,
|
||||
description: form.description,
|
||||
account_number: form.account_number,
|
||||
account_name: form.account_name,
|
||||
corp_id: form.corp_id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await financeService.accountEdit({
|
||||
id: form.id,
|
||||
currency_id: form.currency_id,
|
||||
short_desc: form.short_desc,
|
||||
status: form.status,
|
||||
sort: form.sort,
|
||||
bank_name: form.bank_name,
|
||||
bank_branch: form.bank_branch,
|
||||
description: form.description,
|
||||
account_number: form.account_number,
|
||||
account_name: form.account_name,
|
||||
corp_id: form.corp_id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getAccountList()
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getCurrencyLabel = (id: number | undefined) => {
|
||||
if (!id) return '-'
|
||||
const found = currencyOptions.value.find((c: any) => c.id === id)
|
||||
return found ? `${found.currency_name} (${found.currency_code})` : 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;
|
||||
}
|
||||
.account-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.account-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.account-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
.el-col2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="customer-page">
|
||||
<div class="customer-toolbar boxshadow">
|
||||
<div class="customer-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<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('currency@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="currency_code" min-width="140" />
|
||||
<ElTableColumn label="货币名称" prop="currency_name" min-width="140" />
|
||||
<ElTableColumn label="符号" prop="currency_symbol" width="100" />
|
||||
<ElTableColumn label="当前汇率" prop="exchange_rate" min-width="120" />
|
||||
<ElTableColumn label="允许上浮动%" prop="float_percent_up" min-width="120" />
|
||||
<ElTableColumn label="允许下浮动%" prop="float_percent_down" min-width="120" />
|
||||
<ElTableColumn label="状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '停用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
|
||||
<ElTableColumn fixed="right" label="操作" width="240px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showDetail(scope.row)" v-if="hasAuth('currency@show')">
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('currency@update')"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleDelete(scope.row)"
|
||||
v-if="hasAuth('currency@destroy')"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增货币' : '编辑货币'"
|
||||
align-center
|
||||
width="640px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px" class="form">
|
||||
<ElFormItem label="货币编码" prop="currency_code">
|
||||
<ElInput v-model="form.currency_code" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="货币名称" prop="currency_name">
|
||||
<ElInput v-model="form.currency_name" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="货币符号" prop="currency_symbol">
|
||||
<ElInput v-model="form.currency_symbol" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="当前汇率" prop="exchange_rate">
|
||||
<ElInputNumber
|
||||
v-model="form.exchange_rate"
|
||||
:min="0"
|
||||
:step="0.0001"
|
||||
:precision="6"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="允许上浮动%" prop="float_percent_up">
|
||||
<ElInputNumber
|
||||
v-model="form.float_percent_up"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="允许下浮动%" prop="float_percent_down">
|
||||
<ElInputNumber
|
||||
v-model="form.float_percent_down"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:step="0.01"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<ElDialog v-model="detailVisible" title="货币详情" align-center width="640px">
|
||||
<div class="detail-dialog">
|
||||
<div class="section">
|
||||
<div class="info-grid">
|
||||
<div class="label">货币编码</div>
|
||||
<div class="value">{{ detailData.currency_code || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">货币名称</div>
|
||||
<div class="value">{{ detailData.currency_name || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">货币符号</div>
|
||||
<div class="value">{{ detailData.currency_symbol || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">当前汇率</div>
|
||||
<div class="value">{{ detailData.exchange_rate ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">允许上浮动%</div>
|
||||
<div class="value">{{ detailData.float_percent_up ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">允许下浮动%</div>
|
||||
<div class="value">{{ detailData.float_percent_down ?? '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">状态</div>
|
||||
<div class="value">{{ detailData.status ? '启用' : '停用' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">创建时间</div>
|
||||
<div class="value">{{ detailData.created_at || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">更新时间</div>
|
||||
<div class="value">{{ detailData.updated_at || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="detailVisible = false">关闭</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { financeService } from '@/api/financeApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'TypeCurrency' })
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const keyword = ref<string>('')
|
||||
const rules = reactive<FormRules>({
|
||||
currency_code: [{ required: true, message: '请输入货币编码', trigger: 'blur' }],
|
||||
currency_name: [{ required: true, message: '请输入货币名称', trigger: 'blur' }],
|
||||
exchange_rate: [{ required: true, message: '请输入当前汇率', trigger: 'change' }]
|
||||
})
|
||||
const form = reactive<any>({
|
||||
id: '',
|
||||
currency_code: '',
|
||||
currency_name: '',
|
||||
currency_symbol: '',
|
||||
exchange_rate: 0,
|
||||
float_percent_up: 0,
|
||||
float_percent_down: 0,
|
||||
status: 1
|
||||
})
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
onMounted(() => {
|
||||
getCurrencyList()
|
||||
})
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
getCurrencyList()
|
||||
})
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getCurrencyList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getCurrencyList()
|
||||
}
|
||||
// 获取货币列表
|
||||
const getCurrencyList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const { records, current, size, total } = await financeService.getCurrencyList({
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
})
|
||||
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 detailVisible = ref(false)
|
||||
const detailData = ref<any>({})
|
||||
const showDialog = (type: 'add' | 'edit', row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
currency_code: row.currency_code,
|
||||
currency_name: row.currency_name,
|
||||
currency_symbol: row.currency_symbol,
|
||||
exchange_rate: row.exchange_rate ?? 0,
|
||||
float_percent_up: row.float_percent_up ?? 0,
|
||||
float_percent_down: row.float_percent_down ?? 0,
|
||||
status: row.status ?? 1
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
id: '',
|
||||
currency_code: '',
|
||||
currency_name: '',
|
||||
currency_symbol: '',
|
||||
exchange_rate: 0,
|
||||
float_percent_up: 0,
|
||||
float_percent_down: 0,
|
||||
status: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleDelete = (row?: any) => {
|
||||
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await financeService.currencyDelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getCurrencyList()
|
||||
})
|
||||
}
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (dialogType.value === 'add') {
|
||||
const { id, ...payload } = form
|
||||
const { message } = await financeService.currencyAdd(payload)
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await financeService.currencyEdit(form)
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getCurrencyList()
|
||||
})
|
||||
}
|
||||
|
||||
const showDetail = async (row: any) => {
|
||||
detailVisible.value = true
|
||||
const res = await financeService.currencyDetail({ id: row.id })
|
||||
detailData.value = res || {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.customer-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.page-content {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
.customer-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.customer-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.message-text {
|
||||
padding: 10px 14px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
}
|
||||
.detail-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.section {
|
||||
background: var(--el-fill-color-blank);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
gap: 6px 12px;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
.content-box {
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,907 @@
|
||||
<!-- eslint-disable vue/no-parsing-error -->
|
||||
<!-- eslint-disable vue/valid-attribute-name -->
|
||||
<!-- eslint-disable prettier/prettier -->
|
||||
<template>
|
||||
<div class="boxshadow">
|
||||
<section class="search-bar art-custom-card">
|
||||
<el-form v-model:filter="formFilters" label-position="left">
|
||||
<el-row class="search-form-row" :gutter="12">
|
||||
<el-col :xs="24" :sm="12" :md="4" :lg="4" :xl="4">
|
||||
<el-form-item label="关键字" label-width="70px">
|
||||
<el-input v-model="filter.keyword" placeholder="请输入关键字" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="4" :lg="4" :xl="4">
|
||||
<el-form-item label="状态" label-width="70px">
|
||||
<el-select v-model="filter.status">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="待发布" value="0" />
|
||||
<el-option label="待跟踪" value="1" />
|
||||
<el-option label="已跟踪" value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="4" :lg="4" :xl="4">
|
||||
<el-form-item label="渠道" label-width="70px">
|
||||
<el-select v-model="filter.channel">
|
||||
<el-option v-for="v in channelOptions" :key="v.id" :label="v.name" :value="v.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :md="6" :lg="6" :xl="6">
|
||||
<el-form-item label="时间" label-width="70px">
|
||||
<el-date-picker
|
||||
v-model="filter.time"
|
||||
type="daterange"
|
||||
format="YYYY-MM-DD"
|
||||
valueFormat="YYYY-MM-DD"
|
||||
rangeSeparator="至"
|
||||
startPlaceholder="开始日期"
|
||||
endPlaceholder="结束日期"
|
||||
:style="{ width: '100%' }"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :xs="24" :sm="24" :md="4" :lg="4" :xl="4" class="action-column">
|
||||
<div
|
||||
class="action-buttons-wrapper"
|
||||
:style="{
|
||||
'justify-content': isMobile ? 'flex-end' : 3 <= 2 ? 'flex-start' : 'flex-end'
|
||||
}"
|
||||
>
|
||||
<div class="form-buttons">
|
||||
<el-button class="reset-button" @click="handleReset">{{
|
||||
$t('table.searchBar.reset')
|
||||
}}</el-button>
|
||||
<el-button type="primary" class="search-button" @click="handleSearch">{{
|
||||
$t('table.searchBar.search')
|
||||
}}</el-button>
|
||||
</div>
|
||||
<div class="form-btn-right">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</section>
|
||||
<div class="table">
|
||||
<el-button class="reset-button" @click="addfn()" v-if="hasAuth('inquiry#store')"><i class="iconfont-sys"></i> 录单</el-button>
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
@selection-change="handleSelectionChange"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<el-table-column prop="order_no" label="SN"></el-table-column>
|
||||
<el-table-column prop="contacts" label="客户名称"></el-table-column>
|
||||
<el-table-column prop="tel" label="电话"></el-table-column>
|
||||
<el-table-column prop="phone" label="国家">
|
||||
<template #default="scope">
|
||||
<span>
|
||||
{{scope.row.countryInfo? scope.row.countryInfo.cn_addr : ''}}
|
||||
</span>
|
||||
<span v-if="scope.row.countryInfo">
|
||||
<el-tag class="steer" type="warning" v-if="scope.row.countryInfo.steering=='左舵'">{{ scope.row.countryInfo.steering }}</el-tag>
|
||||
<el-tag class="steer" type="success" v-else-if="scope.row.countryInfo.steering=='右舵'">{{ scope.row.countryInfo.steering }}</el-tag>
|
||||
</span>
|
||||
<!-- <span class="steer" v-if="scope.row.countryInfo">{{ scope.row.countryInfo.steering }}</span> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="渠道">
|
||||
<template #default="scope">
|
||||
<span>
|
||||
{{scope.row.channelInfo? scope.row.channelInfo.name : ''}}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quality" label="客户商机">
|
||||
<template #default="scope">
|
||||
<span :style="scope.row.quality==1 ? { color: '#F5AB00' } : scope.row.quality==2 ? { color: '#8495B8' }: { color: '#9B7200' }">
|
||||
{{scope.row.quality==1 ? '金牌商机' : scope.row.quality==2 ? '银牌商机' : '铜牌商机'}}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="magnitude" label="发布人">
|
||||
<template #default="scope">
|
||||
<span>
|
||||
{{scope.row.enter? scope.row.enter.name : ''}}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="recipient" label="跟单人">
|
||||
<template #default="scope">
|
||||
<span style="color:#008477;cursor: pointer;" v-if="scope.row.recipientInfo && hasAuth('inquiry#reallocation')" @click="onFenpei(scope.row,'edit')">
|
||||
{{scope.row.recipientInfo? scope.row.recipientInfo.name : ''}}
|
||||
<i class="iconfont-sys"></i>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态">
|
||||
<template #default="scope">
|
||||
<span :style="scope.row.status== 0 ? { color: '#ccc' } :scope.row.status== 1 ? { color: '#00A43F' }: { color: '#3471FF' }">
|
||||
{{ scope.row.status== 0 ? '未发布' : scope.row.status== 1 ? '待跟踪' : '已跟踪' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="倒计时" min-width="140">
|
||||
<template #default="scope">
|
||||
<span :class="getCountdownClass(scope.row.countdown)" v-if="scope.row.status==1">
|
||||
<span class="custom-clock-icon" style="margin-right: 4px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
{{ getCountdown(scope.row.countdown) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="260px">
|
||||
<template #default="scope">
|
||||
<div class="follow-action fenpei" @click="onFenpei(scope.row,'add')" v-if="scope.row.status!= 2&&hasAuth('inquiry#allocation')">
|
||||
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
分配
|
||||
</div>
|
||||
<div class="follow-action detil" @click="onDetail(scope.row)" v-if="hasAuth('inquiry#show')">
|
||||
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
详情
|
||||
</div>
|
||||
<div class="follow-action" @click="onFollow(scope.row)" v-if="scope.row.status==0&&hasAuth('inquiry#release')">
|
||||
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
发布
|
||||
</div>
|
||||
<div class="follow-action edit" @click="onEdit(scope.row)" v-if="hasAuth('inquiry#update')">
|
||||
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
编辑
|
||||
</div>
|
||||
<div class="follow-action delete" @click="onDelete(scope.row)" v-if="hasAuth('inquiry#destroy')">
|
||||
<span class="custom-clock-icon" style="margin-right: 2px; vertical-align: middle"
|
||||
><i class="iconfont-sys"></i></span
|
||||
>
|
||||
删除
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</div>
|
||||
<el-dialog v-model="detailDialogVisible" title="详情" >
|
||||
<div class="detail-info-table">
|
||||
<div class="row">
|
||||
<div>客户名称:{{ detailRow.contacts }}</div>
|
||||
<div>电话:{{ detailRow.tel }}</div>
|
||||
<div>国家:{{ detailRow.countryInfo? detailRow.countryInfo.cn_addr:'' }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>渠道:{{ detailRow.channelInfo?detailRow.channelInfo.name:'' }}</div>
|
||||
<div>邮箱:{{ detailRow.email }}</div>
|
||||
<div>WhatsApp:{{ detailRow.whatsapp }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>发布人:{{ detailRow.enter? detailRow.enter.name : ''}}</div>
|
||||
<div>跟单人:{{ detailRow.recipientInfo? detailRow.recipientInfo.name : ''}}</div>
|
||||
<div>录单时间:{{ detailRow.created_at }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
等级:
|
||||
<span :style="detailRow.quality==1 ? { color: '#F5AB00' } : detailRow.quality==2 ? { color: '#8495B8' }: { color: '#9B7200' }">
|
||||
{{detailRow.quality==1 ? '金牌商机' : detailRow.quality==2 ? '银牌商机' : '铜牌商机'}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
>状态:<span>{{ detailRow.status== 0 ? '未发布' : detailRow.status== 1 ? '待跟踪' : '已跟踪' }}</span></div
|
||||
>
|
||||
<div>SN:{{ detailRow.order_no }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dtitle">详情内容</div>
|
||||
<div v-if="detailRow" v-html="detailRow.content" class="detils-content" @click="handleImageClick" style="cursor: pointer;">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 图片预览组件 -->
|
||||
<ElImageViewer
|
||||
v-if="showImageViewer"
|
||||
:url-list="imageList"
|
||||
:initial-index="currentImageIndex"
|
||||
:hide-on-click-modal="true"
|
||||
@close="showImageViewer = false"
|
||||
/>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
:title="dialogType === 'add' ? '录单' : '编辑'"
|
||||
v-model="AddDialogVisible"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="addForm" ref="addFormRef" label-width="80px" class="add-form">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="渠道" prop="channel">
|
||||
<el-select v-model="addForm.channel" placeholder="请选择渠道">
|
||||
<el-option
|
||||
v-for="item in channelOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="等级" prop="quality">
|
||||
<el-select v-model="addForm.quality" placeholder="请选择等级">
|
||||
<el-option
|
||||
v-for="item in opportunityOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="国家" prop="country">
|
||||
<el-select v-model="addForm.country" filterable placeholder="请选择国家">
|
||||
<el-option
|
||||
v-for="item in countryOptions"
|
||||
:key="item.id"
|
||||
:label="item.cn_addr"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="Email" prop="email">
|
||||
<el-input v-model="addForm.email" placeholder="请输入Email" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系方式" prop="tel">
|
||||
<el-input v-model="addForm.tel" placeholder="请输入联系方式" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系人" prop="contacts">
|
||||
<el-input v-model="addForm.contacts" placeholder="请输入联系人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item label="WhatsApp" prop="whatsapp">
|
||||
<el-input v-model="addForm.whatsapp" placeholder="请输入WhatsApp" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="跟单人" prop="country">
|
||||
<el-select v-model="addForm.recipient" filterable placeholder="请选择跟单人">
|
||||
<el-option
|
||||
v-for="item in userOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="详情内容" prop="content">
|
||||
<ArtWangEditor class="el-top" v-model="editorHtml" />
|
||||
</el-form-item>
|
||||
<div class="form-submit-row">
|
||||
<el-button type="primary" size="large" style="width: 50%" @click="handleAddSubmit"
|
||||
>提交</el-button
|
||||
>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
<!-- 分配 -->
|
||||
<el-dialog v-model="fenpeiDialogVisible" :title="dialogTypefp === 'add' ? '分配跟单' : '重新分配跟单'" width="360px">
|
||||
<el-form :model="fenpeiForm" label-width="0">
|
||||
<el-form-item>
|
||||
<el-select v-model="fenpeiForm.recipient" 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>
|
||||
<div style="display: flex; justify-content: space-around; margin-top: 24px">
|
||||
<el-button @click="fenpeiDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleFenpeiSubmit">确定</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { ElMessageBox, ElImageViewer } from 'element-plus'
|
||||
import { CrmService } from '@/api/crmApi'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
defineOptions({ name: 'InquirySystem' })
|
||||
const { hasAuth } = useAuth()
|
||||
const loading = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const dialogTypefp = ref('add')
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = computed(() => width.value < 500)
|
||||
const filter = ref({
|
||||
status: '',
|
||||
time: '',
|
||||
channel: '',
|
||||
keyword: ''
|
||||
})
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const editorHtml = ref('') // 编辑器内容
|
||||
const timer = ref(0)
|
||||
const refreshKey = ref(0)
|
||||
const fenpeiDialogVisible = ref(false)
|
||||
const detailDialogVisible = ref(false)
|
||||
const AddDialogVisible = ref(false)
|
||||
const detailRow = ref<any>(null)
|
||||
const countryOptions = ref<any>()
|
||||
const tableData = ref<any[]>([])
|
||||
const addFormRef = ref()
|
||||
let addForm = reactive({
|
||||
channel: '',
|
||||
quality: '',
|
||||
country: '',
|
||||
email: '',
|
||||
tel: '',
|
||||
contacts: '',
|
||||
whatsapp: '',
|
||||
content: '',
|
||||
recipient: ''
|
||||
})
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...filter })
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
filter.value = {
|
||||
status: '',
|
||||
time: '',
|
||||
channel: '',
|
||||
keyword: ''
|
||||
}
|
||||
Object.assign(formFilters, { ...filter })
|
||||
pagination.currentPage = 1 // 重置到第一页
|
||||
gettableList()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('搜索参数:', formFilters)
|
||||
pagination.currentPage = 1 // 搜索时重置到第一页
|
||||
gettableList()
|
||||
}
|
||||
const opportunityOptions = ref<any>()
|
||||
//商机
|
||||
const getMagnitude = async () => {
|
||||
// 这里调用获取品牌列表的接口
|
||||
const res = await CrmService.getMagnitude()
|
||||
opportunityOptions.value = res || []
|
||||
}
|
||||
const channelOptions = ref<any>()
|
||||
//渠道
|
||||
const getgetChannel = async () => {
|
||||
const res = await CrmService.getChannel()
|
||||
channelOptions.value = res
|
||||
}
|
||||
//国家
|
||||
const getarea = async () => {
|
||||
const res = await CrmService.getCountry()
|
||||
countryOptions.value = res || []
|
||||
}
|
||||
const userOptions = ref<any>()
|
||||
const getuserList = async () => {
|
||||
// 这里调用获取用户列表的接口
|
||||
const res = await manageService.presetusers()
|
||||
userOptions.value = res || []
|
||||
// return res || []
|
||||
}
|
||||
// 计算属性:将时间格式化为 "开始日期 - 结束日期"
|
||||
const formattedTimeRange = () => {
|
||||
if (!filter.value.time || filter.value.time.length !== 2) {
|
||||
return '' // 如果时间范围无效,返回空字符串
|
||||
}
|
||||
const [startDate, endDate] = filter.value.time
|
||||
return `${startDate} - ${endDate}`
|
||||
}
|
||||
// 获取列表数据
|
||||
const gettableList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params: any = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
channel: filter.value.channel,
|
||||
status: filter.value.status,
|
||||
keyword: filter.value.keyword,
|
||||
time: formattedTimeRange()
|
||||
}
|
||||
const { records, current, size, total } = await manageService.getinquiryList(params)
|
||||
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 selectedRows = ref<any[]>([])
|
||||
// 处理表格行选择变化
|
||||
const handleSelectionChange = (selection: any[]) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
gettableList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
gettableList()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getgetChannel()
|
||||
gettableList()
|
||||
getuserList()
|
||||
timer.value = window.setInterval(() => {
|
||||
refreshKey.value++
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
}
|
||||
})
|
||||
function getCountdown(targetTimestamp: number) {
|
||||
// 让 refreshKey 成为依赖,确保每秒刷新
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
refreshKey.value
|
||||
const now = dayjs()
|
||||
const target = dayjs.unix(targetTimestamp) // 使用时间戳创建 dayjs 对象
|
||||
if (!target.isValid()) return '时间格式错误'
|
||||
const diff = target.diff(now)
|
||||
if (diff <= 0) return '已超时'
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((diff / (1000 * 60 * 60)) % 24)
|
||||
const minutes = Math.floor((diff / (1000 * 60)) % 60)
|
||||
const seconds = Math.floor((diff / 1000) % 60)
|
||||
return `${days > 0 ? days + '天' : ''}${hours}小时${minutes}分${seconds}秒`
|
||||
}
|
||||
// function getCountdown(targetDate: string) {
|
||||
// // 让 refreshKey 成为依赖,确保每秒刷新
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
// refreshKey.value
|
||||
// const now = dayjs()
|
||||
// const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
|
||||
// if (!target.isValid()) return '时间格式错误'
|
||||
// const diff = target.diff(now)
|
||||
// if (diff <= 0) return '已超时'
|
||||
// const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
// const hours = Math.floor((diff / (1000 * 60 * 60)) % 24)
|
||||
// const minutes = Math.floor((diff / (1000 * 60)) % 60)
|
||||
// const seconds = Math.floor((diff / 1000) % 60)
|
||||
// return `${days > 0 ? days + '天' : ''}${hours}小时${minutes}分${seconds}秒`
|
||||
// }
|
||||
|
||||
function getCountdownClass(targetDate: number) {
|
||||
const now = dayjs()
|
||||
// const target = dayjs(targetDate, 'YYYY-MM-DD HH:mm:ss')
|
||||
const target = dayjs.unix(targetDate)
|
||||
if (!target.isValid() || target.diff(now) <= 0) {
|
||||
return 'countdown-danger'
|
||||
}
|
||||
return 'countdown-success'
|
||||
}
|
||||
function reactiveForm() {
|
||||
Object.assign(addForm, {
|
||||
channel: '',
|
||||
quality: '',
|
||||
country: '',
|
||||
email: '',
|
||||
tel: '',
|
||||
contacts: '',
|
||||
whatsapp: '',
|
||||
content: '',
|
||||
recipient: ''
|
||||
})
|
||||
editorHtml.value = ''
|
||||
}
|
||||
const addfn = () => {
|
||||
dialogType.value = 'add'
|
||||
reactiveForm()
|
||||
getMagnitude()
|
||||
getarea()
|
||||
AddDialogVisible.value = true
|
||||
}
|
||||
const onFollow = async (row: any) => {
|
||||
ElMessageBox.confirm('确定要发布此询单吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await manageService.inquiryrelease({ id: row.id, status: 1 })
|
||||
ElMessage.success(res?.message || '请求成功!')
|
||||
gettableList()
|
||||
// 你可以在这里调用接口或做后续处理
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户点击了取消
|
||||
})
|
||||
}
|
||||
|
||||
function onDetail(row: any) {
|
||||
row.content = processImages(row.content) // 处理图片
|
||||
detailRow.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
//编辑
|
||||
function processImages(html: string) {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, 'text/html')
|
||||
doc.querySelectorAll('img').forEach((img) => {
|
||||
img.style.maxWidth = '100%'
|
||||
img.style.height = 'auto'
|
||||
})
|
||||
return doc.body.innerHTML
|
||||
}
|
||||
const carrid = ref('')
|
||||
const onEdit = async (row: any) => {
|
||||
dialogType.value = 'edit'
|
||||
getMagnitude()
|
||||
getarea()
|
||||
AddDialogVisible.value = true
|
||||
carrid.value = row.id
|
||||
addForm = reactive({
|
||||
channel: row.channelInfo ? row.channelInfo.id : '',
|
||||
quality: row.quality,
|
||||
country: row.countryInfo ? row.countryInfo.id : '',
|
||||
email: row.email,
|
||||
tel: row.tel,
|
||||
contacts: row.contacts,
|
||||
whatsapp: row.whatsapp,
|
||||
content: row.content,
|
||||
recipient: row.recipientInfo ? row.recipientInfo.id : ''
|
||||
})
|
||||
editorHtml.value = row.content
|
||||
// 这里可以添加编辑逻辑
|
||||
// 例如打开编辑对话框或跳转到编辑页面
|
||||
}
|
||||
const delCodeTrim = (content: string): string => {
|
||||
return content.replace(/(\s*)<\/code>/g, '</code>')
|
||||
}
|
||||
const handleAddSubmit = async () => {
|
||||
// 提交表单逻辑
|
||||
// 这里可以调用 API 提交 addForm 数据
|
||||
if (dialogType.value === 'add') {
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
addForm.content = editorHtml.value
|
||||
const res = await manageService.inquiryAdd(addForm)
|
||||
ElMessage.success(res?.message || '录单成功!')
|
||||
} else {
|
||||
editorHtml.value = delCodeTrim(editorHtml.value)
|
||||
addForm.content = editorHtml.value
|
||||
const forms = { ...addForm, id: carrid.value }
|
||||
const res = await manageService.inquiryedit(forms)
|
||||
ElMessage.success(res?.message || '录单成功!')
|
||||
}
|
||||
AddDialogVisible.value = false
|
||||
gettableList()
|
||||
}
|
||||
//删除
|
||||
const onDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确定要删除此条询单吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
const res = await manageService.inquirydelete({ id: row.id })
|
||||
ElMessage.success(res?.message || '提交成功!')
|
||||
gettableList()
|
||||
// 你可以在这里调用接口或做后续处理
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户点击了取消
|
||||
})
|
||||
}
|
||||
|
||||
const fenpeiForm = reactive({
|
||||
id: '',
|
||||
recipient: ''
|
||||
})
|
||||
//分配
|
||||
const onFenpei = (row: any, edits: any) => {
|
||||
dialogTypefp.value = edits
|
||||
fenpeiForm.id = row.id
|
||||
fenpeiDialogVisible.value = true
|
||||
}
|
||||
const handleFenpeiSubmit = async () => {
|
||||
if (!fenpeiForm.recipient) {
|
||||
ElMessage.warning('请选择跟单人')
|
||||
return
|
||||
}
|
||||
if (dialogTypefp.value === 'add') {
|
||||
const res = await manageService.inquiryallocation({
|
||||
id: fenpeiForm.id,
|
||||
recipient: fenpeiForm.recipient,
|
||||
status: 2
|
||||
})
|
||||
ElMessage.success(res?.message || '分配成功!')
|
||||
} else {
|
||||
const res = await manageService.inquiryreallocation({
|
||||
id: fenpeiForm.id,
|
||||
recipient: fenpeiForm.recipient,
|
||||
status: 2
|
||||
})
|
||||
ElMessage.success(res?.message || '分配成功!')
|
||||
}
|
||||
fenpeiDialogVisible.value = false
|
||||
gettableList()
|
||||
}
|
||||
|
||||
const imageList = ref<string[]>([])
|
||||
const currentImageIndex = ref(0)
|
||||
const showImageViewer = ref(false)
|
||||
|
||||
const handleImageClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement
|
||||
if (target.tagName === 'IMG') {
|
||||
const imgElement = target as HTMLImageElement
|
||||
|
||||
// 获取所有图片并设置预览列表
|
||||
const contentDiv = event.currentTarget as HTMLElement
|
||||
const allImages = contentDiv.querySelectorAll('img')
|
||||
const imageUrls = Array.from(allImages).map(img => img.src)
|
||||
|
||||
imageList.value = imageUrls
|
||||
currentImageIndex.value = imageUrls.indexOf(imgElement.src)
|
||||
showImageViewer.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-bar {
|
||||
padding: 20px 20px 0;
|
||||
background-color: var(--art-main-bg-color);
|
||||
border-radius: calc(var(--custom-radius) / 2 + 2px);
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.search-form-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.action-column {
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
|
||||
.action-buttons-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
// justify-content: flex-end;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filter-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
line-height: 32px;
|
||||
color: var(--main-color);
|
||||
cursor: pointer;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-btn-right {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
.card-list {
|
||||
.art-custom-card {
|
||||
.iconfont-sys {
|
||||
background-color: #232323 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-top: 10px;
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 300px !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header th) {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
background: #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr.custom-table-row td) {
|
||||
border-right: 1px solid #f7f9fa !important;
|
||||
border-bottom: 1px solid #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr td:last-child) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body tr:last-child td) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th) {
|
||||
border-right: 1px solid #f7f9fa !important;
|
||||
border-bottom: 1px solid #f7f9fa !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__header-wrapper th:last-child) {
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
border: 1px solid #f7f9fa !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:deep(.el-table th),
|
||||
:deep(.el-table td) {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
.countdown-danger {
|
||||
color: #f00 !important;
|
||||
}
|
||||
|
||||
.countdown-success {
|
||||
color: #00a43f !important;
|
||||
}
|
||||
|
||||
.fenpei {
|
||||
color: #3471ff !important;
|
||||
}
|
||||
|
||||
.detil {
|
||||
color: #00a43f !important;
|
||||
}
|
||||
|
||||
.edit {
|
||||
color: #008477 !important;
|
||||
}
|
||||
|
||||
.delete {
|
||||
color: #f00 !important;
|
||||
}
|
||||
|
||||
.follow-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-info-table {
|
||||
padding-left: 25px;
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 6px;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 16px;
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-submit-row {
|
||||
text-align: center;
|
||||
}
|
||||
.steer {
|
||||
margin-left: 5px;
|
||||
// display: inline-block;
|
||||
// border: 1px solid #00a43f;
|
||||
// padding: 0px 4px;
|
||||
// color: #00a43f;
|
||||
}
|
||||
.dtitle {
|
||||
line-height: 40px;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
padding-left: 25px;
|
||||
}
|
||||
.detils-content {
|
||||
padding-left: 25px;
|
||||
img {
|
||||
max-width: 100% !important;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<div class="color-page">
|
||||
<div class="color-toolbar boxshadow">
|
||||
<div class="color-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input
|
||||
style="min-width: 120px"
|
||||
v-model="keyword"
|
||||
placeholder="请输入关键字"
|
||||
clearable
|
||||
@keyup.enter="getannualList"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getannualList" v-ripple>搜索</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<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('kpi_annual@add')"
|
||||
type="primary"
|
||||
>
|
||||
新增
|
||||
</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="corp_id">
|
||||
<template #default="scope">
|
||||
<p> {{ scope.row.corp_name ? scope.row.corp_name : '' }}</p>
|
||||
</template>
|
||||
</ElTableColumn> -->
|
||||
<ElTableColumn label="年份" prop="year" />
|
||||
<ElTableColumn label="业务员" prop="user_name" />
|
||||
<ElTableColumn label="年度目标数" prop="annual_target" />
|
||||
<ElTableColumn label="创建时间" prop="created_at" />
|
||||
<ElTableColumn fixed="right" label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('kpi_annual@update')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteColor(scope.row)"
|
||||
v-if="hasAuth('kpi_annual@destroy')"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增' : '编辑'"
|
||||
align-center
|
||||
width="600px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
|
||||
<!-- <ElFormItem label="公司" prop="corp_id">
|
||||
<ElSelect v-model="form.corp_id" placeholder="请选择公司">
|
||||
<ElOption
|
||||
v-for="item in companyList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
></ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem> -->
|
||||
<ElFormItem label="年份" prop="year">
|
||||
<el-input v-model="form.year" placeholder="请输入年份" />
|
||||
<!-- <ElInputNumber v-model="form.year" :min="1970" :max="2999" style="width: 100%" /> -->
|
||||
</ElFormItem>
|
||||
<ElFormItem label="业务员" prop="user_id">
|
||||
<ElSelect v-model="form.user_id" filterable placeholder="请选择业务员">
|
||||
<el-option
|
||||
v-for="item in companyUserList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<el-text class="mx-1" type="warning">不选业务员表示公司年度总目标</el-text>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="年度目标数" prop="annual_target">
|
||||
<ElInputNumber v-model="form.annual_target" :min="0" :max="9999999" style="width: 100%" />
|
||||
</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 { kpiService } from '@/api/admin/kpi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'AdminKpiAnnual' })
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const keyword = ref<string>('')
|
||||
// 获取公司列表
|
||||
const companyList = ref<any>()
|
||||
const getCompanyList = async () => {
|
||||
const response = await UserService.getCompanyList()
|
||||
companyList.value = response || []
|
||||
}
|
||||
const companyUserList = ref<any>()
|
||||
// 获取用户列表
|
||||
const getCompanyUserList = async () => {
|
||||
const response = await manageService.presetusers()
|
||||
companyUserList.value = response || []
|
||||
}
|
||||
// 表单数据
|
||||
const form = reactive<any>({
|
||||
id: '',
|
||||
year: new Date().getFullYear(),
|
||||
user_id: '',
|
||||
annual_target: 0
|
||||
})
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
year: [{ required: true, message: '请输入年份', trigger: 'blur' }],
|
||||
annual_target: [{ required: true, message: '请输入年度目标数', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
getannualList()
|
||||
})
|
||||
|
||||
// 监听搜索条件变化
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
getannualList()
|
||||
})
|
||||
|
||||
// 获取颜色列表
|
||||
const getannualList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await kpiService.getyearlylist(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
|
||||
getannualList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getannualList()
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
getCompanyList()
|
||||
getCompanyUserList()
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
// 编辑模式
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
year: row.year,
|
||||
user_id: row.user_id,
|
||||
annual_target: row.annual_target
|
||||
})
|
||||
} else {
|
||||
// 新增模式
|
||||
Object.assign(form, {
|
||||
id: '',
|
||||
year: '',
|
||||
user_id: '',
|
||||
annual_target: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除年度目标
|
||||
const deleteColor = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该年度目标吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const { message } = await kpiService.deleteyearly({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getannualList()
|
||||
} 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 kpiService.addyearly({
|
||||
year: form.year,
|
||||
user_id: form.user_id,
|
||||
annual_target: form.annual_target
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
// 编辑
|
||||
const { message } = await kpiService.updateyearly({
|
||||
id: form.id,
|
||||
year: form.year,
|
||||
user_id: form.user_id,
|
||||
annual_target: form.annual_target
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getannualList()
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.color-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.color-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.color-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.el-col2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div class="color-page">
|
||||
<div class="color-toolbar boxshadow">
|
||||
<div class="color-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input
|
||||
style="min-width: 120px"
|
||||
v-model="keyword"
|
||||
placeholder="请输入关键字"
|
||||
clearable
|
||||
@keyup.enter="getMonthlyList"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getMonthlyList" v-ripple>搜索</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<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('kpi_monthly@add')"
|
||||
type="primary"
|
||||
>
|
||||
新增
|
||||
</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="user_name" min-width="120" />
|
||||
<ElTableColumn label="年份" prop="year" min-width="120" />
|
||||
<ElTableColumn label="月份" prop="month" min-width="120" />
|
||||
<ElTableColumn label="本月销售数" prop="achieved" min-width="140" />
|
||||
<ElTableColumn fixed="right" label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('kpi_monthly@update')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteMonthly(scope.row)"
|
||||
v-if="hasAuth('kpi_monthly@destroy')"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增月度-业绩' : '编辑月度-业绩'"
|
||||
align-center
|
||||
width="600px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
|
||||
<ElFormItem label="用户" prop="user_id">
|
||||
<ElSelect v-model="form.user_id" filterable placeholder="请选择用户">
|
||||
<ElOption
|
||||
v-for="item in companyUserList"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
:label="item.name"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="年份" prop="year">
|
||||
<ElInputNumber v-model="form.year" :min="1970" :max="2999" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="月份" prop="month">
|
||||
<ElInputNumber v-model="form.month" :min="1" :max="12" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="本月销售数" prop="achieved">
|
||||
<ElInputNumber v-model="form.achieved" :min="0" :max="999999" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<!-- 预留字段:目标数,不显示,仅占位保存 -->
|
||||
<input type="hidden" v-model="form.target" />
|
||||
</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 { kpiService } from '@/api/admin/kpi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'AdminKpiMonthly' })
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const keyword = ref<string>('')
|
||||
const companyUserList = ref<any>()
|
||||
// 获取用户列表
|
||||
const getCompanyUserList = async () => {
|
||||
const response = await manageService.presetusers()
|
||||
companyUserList.value = response || []
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<any>({
|
||||
id: '',
|
||||
user_id: '',
|
||||
year: new Date().getFullYear(),
|
||||
month: new Date().getMonth() + 1,
|
||||
achieved: 0,
|
||||
target: undefined
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
user_id: [{ required: true, message: '请输入用户ID', trigger: 'blur' }],
|
||||
year: [{ required: true, message: '请输入年份', trigger: 'change' }],
|
||||
month: [{ required: true, message: '请输入月份', trigger: 'change' }],
|
||||
achieved: [{ required: true, message: '请输入本月销售数', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
getMonthlyList()
|
||||
})
|
||||
|
||||
// 监听搜索条件变化
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
getMonthlyList()
|
||||
})
|
||||
|
||||
// 获取月度业绩列表
|
||||
const getMonthlyList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await kpiService.getmonthlylist(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
|
||||
getMonthlyList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getMonthlyList()
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
getCompanyUserList()
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
// 编辑模式
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
user_id: row.user_id,
|
||||
year: row.year,
|
||||
month: row.month,
|
||||
achieved: row.achieved,
|
||||
target: row.target
|
||||
})
|
||||
} else {
|
||||
// 新增模式
|
||||
Object.assign(form, {
|
||||
id: '',
|
||||
user_id: '',
|
||||
year: new Date().getFullYear(),
|
||||
month: new Date().getMonth() + 1,
|
||||
achieved: 0,
|
||||
target: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除月度业绩
|
||||
const deleteMonthly = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该记录吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const { message } = await kpiService.deletemonthly({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getMonthlyList()
|
||||
} 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 kpiService.addmonthly({
|
||||
user_id: form.user_id,
|
||||
year: form.year,
|
||||
month: form.month,
|
||||
achieved: form.achieved
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
// 编辑
|
||||
const { message } = await kpiService.updatemonthly({
|
||||
id: form.id,
|
||||
user_id: form.user_id,
|
||||
year: form.year,
|
||||
month: form.month,
|
||||
achieved: form.achieved
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getMonthlyList()
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.color-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.color-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.color-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.el-col2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,466 @@
|
||||
<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"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="getList"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getList"><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="order_no" min-width="140" />
|
||||
<ElTableColumn label="阿里订单号/PI号" prop="pi_no" min-width="160" />
|
||||
<ElTableColumn label="客户名称" prop="customer_snapshot" min-width="120" />
|
||||
<ElTableColumn label="客户来源" prop="customer_source_snapshot" min-width="120" />
|
||||
<ElTableColumn label="业务员" prop="salesperson_snapshot" min-width="100" />
|
||||
<ElTableColumn label="日期" prop="created_at" min-width="120" />
|
||||
<ElTableColumn label="状态" prop="approval_status">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.appro_status">
|
||||
<div
|
||||
><span :class="statusClass(scope.row.appro_status.status)">{{ scope.row.appro_status.status_desc ? scope.row.appro_status.status_desc : '' }}</span>
|
||||
<span v-if="scope.row.appro_status.name != ''"
|
||||
>【{{ scope.row.appro_status.name }}】</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" fixed="right" min-width="120" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="10" justify="center">
|
||||
<el-link
|
||||
type="warning"
|
||||
underline="never"
|
||||
v-if="scope.row.file_url != null && scope.row.file_url != ''"
|
||||
@click="printfn(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 打印预览
|
||||
</el-link>
|
||||
<el-link type="primary" underline="never" @click="handleView1(scope.row)">
|
||||
<i class="iconfont-sys"></i> 查看终版
|
||||
</el-link>
|
||||
<el-link type="warning" underline="never" @click="showDialogtimeline(scope.row)">
|
||||
<i class="iconfont-sys"></i> 查看审批
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
<!-- 订单详情弹窗(使用公共内容组件) -->
|
||||
<el-dialog
|
||||
v-model="orderDialog.visible"
|
||||
:title="orderDialog.title"
|
||||
width="95%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<OrderDetailContent :orderData="orderDialog.orderData" />
|
||||
<div class="boxshadow">
|
||||
<div class="form-title">
|
||||
<!-- <span class="border"></span> -->
|
||||
<span class="title">合同</span>
|
||||
</div>
|
||||
<div class="file-preview">
|
||||
<div v-if="contractFile" class="contract-file">
|
||||
<div v-if="isImageFile(contractFile)" class="image-preview">
|
||||
<img
|
||||
:src="contractFile.url"
|
||||
:alt="contractFile.name"
|
||||
class="preview-image"
|
||||
@click="previewImage(contractFile)"
|
||||
/>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ contractFile.name }}</span>
|
||||
<el-button type="text" size="small" @click="previewImage(contractFile)">
|
||||
预览
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="document-preview">
|
||||
<div class="file-icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ contractFile.name }}</span>
|
||||
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
|
||||
下载
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-file">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>暂无合同文件</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px">
|
||||
<el-button @click="orderDialog.visible = false">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- PDF 预览弹窗 -->
|
||||
<el-dialog
|
||||
v-model="pdf.visible"
|
||||
title="打印预览"
|
||||
width="80%"
|
||||
top="40px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div style="max-height: 70vh; display: flex; flex-direction: column; gap: 12px">
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px">
|
||||
<el-button @click="download">下载</el-button>
|
||||
</div>
|
||||
<div
|
||||
v-if="pdf.images.length"
|
||||
style="overflow: auto; border: 1px solid #eee; border-radius: 6px; padding: 8px"
|
||||
>
|
||||
<img
|
||||
v-for="(img, idx) in pdf.images"
|
||||
:key="idx"
|
||||
:src="img"
|
||||
style="display: block; width: 100%; margin-bottom: 8px; background: #fff"
|
||||
/>
|
||||
</div>
|
||||
<iframe
|
||||
v-else-if="pdf.blobUrl || pdf.url"
|
||||
ref="frameRef"
|
||||
:src="pdf.blobUrl || pdf.url"
|
||||
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="timelineDialogVisible"
|
||||
title="审批历史"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="approval-dialog"
|
||||
>
|
||||
<div class="approval-content">
|
||||
<!-- 头部信息 -->
|
||||
<ApprovalTimeline :records="approvalRecords" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { router } from '@/router'
|
||||
// import { CrmService } from '@/api/crmApi'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
|
||||
import ApprovalTimeline from '@/components/custom/ApprovalTimeline.vue'
|
||||
import type { OrderData } from '@/composables/useOrderDetailDialog'
|
||||
import { usePdfPreview } from '@/composables/usePdfPreview'
|
||||
const timelineDialogVisible = ref(false)
|
||||
const approvalRecords = ref<any[]>([])
|
||||
// PDF 预览/打印(公共)
|
||||
const { pdf, frameRef, openWithResponse, download, print, preparePreview, prepareImages } = usePdfPreview()
|
||||
defineOptions({ name: 'AdminFinalListPage' })
|
||||
// 订单详情弹窗
|
||||
const orderDialog = reactive({
|
||||
visible: false,
|
||||
title: '订单详情',
|
||||
orderData: {} as OrderData
|
||||
})
|
||||
|
||||
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 })
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
is_final: 1, // 只获取备货单,非终版
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await manageService.getStockList(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
function statusClass(status: any) {
|
||||
if (status === 'rejected') return 'status-lost'
|
||||
if (status === 'approved') return 'status-following'
|
||||
return 'status-dealed'
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
getList()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getList()
|
||||
}
|
||||
//打印
|
||||
const printfn = (row: any) => {
|
||||
manageService
|
||||
.printStock({ id: row.id })
|
||||
.then((res) => {
|
||||
openWithResponse(res)
|
||||
// 优先转成图片以适配不带插件的环境
|
||||
prepareImages()
|
||||
// 同时保留 blob 预览作为兜底
|
||||
preparePreview()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
|
||||
const handleView1 = async (row: any) => {
|
||||
const res = await manageService.checkStock({ id: row.object_id })
|
||||
if (res?.contract) {
|
||||
contractFile.value = {
|
||||
name: '合同文件',
|
||||
url: res.contract_url || res.contract,
|
||||
file_name: res.contract
|
||||
}
|
||||
}
|
||||
if (res) {
|
||||
// 获取订单详情成功
|
||||
const orderData: OrderData = res
|
||||
orderDialog.orderData = orderData
|
||||
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
|
||||
orderDialog.visible = true
|
||||
}
|
||||
// router.push({
|
||||
// path: '/admin/stock/final_show_admin',
|
||||
// query: { id: row.id } // 可带客户名等参数
|
||||
// })
|
||||
// TODO: 跳转或弹窗查看备货单
|
||||
}
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 检查文件名
|
||||
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查file_name字段
|
||||
if (file.file_name && file.file_name.startsWith('image/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查URL中的文件扩展名(去除查询参数)
|
||||
if (file.url) {
|
||||
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
|
||||
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 创建图片预览弹窗
|
||||
const imagePreview = document.createElement('div')
|
||||
imagePreview.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const image = document.createElement('img')
|
||||
image.src = file.url
|
||||
image.alt = file.name
|
||||
image.style.cssText = `
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`
|
||||
|
||||
const closeBtn = document.createElement('div')
|
||||
closeBtn.innerHTML = '×'
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
`
|
||||
|
||||
imagePreview.appendChild(image)
|
||||
imagePreview.appendChild(closeBtn)
|
||||
document.body.appendChild(imagePreview)
|
||||
|
||||
// 点击关闭
|
||||
const closePreview = () => {
|
||||
document.body.removeChild(imagePreview)
|
||||
}
|
||||
|
||||
imagePreview.addEventListener('click', closePreview)
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
closePreview()
|
||||
})
|
||||
|
||||
// ESC键关闭
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closePreview()
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = file.url
|
||||
link.download = file.name
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
//审批进度
|
||||
const showDialogtimeline = async (row: any) => {
|
||||
// 审批记录(兼容多种字段)
|
||||
const resstockapprovallogs = await manageService.stockapprovaltimeline({
|
||||
id: row.id,
|
||||
flow_type: 'stock_orders'
|
||||
})
|
||||
console.log(resstockapprovallogs)
|
||||
const historyList = resstockapprovallogs
|
||||
if (historyList.length > 0) {
|
||||
approvalRecords.value = historyList.map((h: any) => ({
|
||||
name: h?.initiator_name ? h?.initiator_name : h?.name,
|
||||
role: h?.status_desc,
|
||||
time: h?.time || '',
|
||||
approved: h?.status === 'approved' || h?.status === 'submitted',
|
||||
rejected: h?.status === 'rejected',
|
||||
pending: h?.status != 'approved' && h?.status != 'rejected' && h?.status != 'submitted',
|
||||
comment: h?.comment || h?.remark || ''
|
||||
}))
|
||||
}
|
||||
timelineDialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stocking-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.stocking-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clickable { cursor: pointer; }
|
||||
.status-following {
|
||||
color: #00a43f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-dealed {
|
||||
color: #3471ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-lost {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.no-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-file i {
|
||||
font-size: 48px;
|
||||
color: #ddd;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,678 @@
|
||||
<template>
|
||||
<div class="stocking-show-page">
|
||||
<div class="header boxshadow">
|
||||
<div class="header-left">
|
||||
<div class="title">合塑实业·汽车事业部-备货单(客户下定)</div>
|
||||
<div class="sub">
|
||||
<span class="mr40">备货单编号:{{ header.order_no }}</span>
|
||||
<span>业务员:{{ header.salesman }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ElButton type="primary" plain @click="printCurrent">打印整单</ElButton>
|
||||
</div>
|
||||
|
||||
<!-- 基础信息 -->
|
||||
<div class="boxshadow">
|
||||
<div class="form-title"><span class="border"></span><span class="title">基础信息</span></div>
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="阿里订单号/PI号">
|
||||
<ElInput v-model="base.pn_no" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="日期">
|
||||
<ElInput v-model="base.date" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="清关目的国">
|
||||
<ElInput v-model="base.close_date" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="3">
|
||||
<ElFormItem label="客户来源">
|
||||
<ElInput v-model="base.channel" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="客户名称">
|
||||
<ElInput v-model="base.customer_name" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="客户所在国">
|
||||
<ElInput v-model="base.client_country" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情 -->
|
||||
<div class="boxshadow">
|
||||
<div class="form-title"><span class="border"></span><span class="title">订单详情</span></div>
|
||||
<div class="items-wrap">
|
||||
<div class="item-row" v-for="(item, idx) in items" :key="idx">
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="车型" label-width="64px">
|
||||
<ElInput v-model="item.model" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="颜色/外" label-width="72px">
|
||||
<ElInput v-model="item.color_out" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="内饰" label-width="52px">
|
||||
<ElInput v-model="item.color_in" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="改装" label-width="52px">
|
||||
<ElInput v-model="item.modify" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="单价" label-width="52px">
|
||||
<ElInput v-model="item.unit_price" disabled>
|
||||
<template #append>{{ item.unit_price_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="总价" label-width="52px">
|
||||
<ElInput v-model="item.total_price" disabled>
|
||||
<template #append>{{ item.total_price_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="改装费" label-width="60px">
|
||||
<ElInput v-model="item.modify_fee" disabled>
|
||||
<template #append>{{ item.modify_fee_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="FOB段运费" label-width="84px">
|
||||
<ElInput v-model="item.fob_fee" disabled>
|
||||
<template #append>{{ item.fob_fee_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="手续费" label-width="60px">
|
||||
<ElInput v-model="item.helper_fee" disabled>
|
||||
<template #append>{{ item.helper_fee_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="港杂费" label-width="60px">
|
||||
<ElInput v-model="item.port_fee" disabled>
|
||||
<template #append>{{ item.port_fee_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="里程" label-width="52px">
|
||||
<ElInput v-model="item.mileage" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<template v-if="(item.mileage ?? 0) <= 0">
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="官方指导价" label-width="85px">
|
||||
<ElInput v-model="item.guide_price" disabled>
|
||||
<template #append>{{ item.guide_price_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="优惠" label-width="52px">
|
||||
<ElInput v-model="item.discount" disabled>
|
||||
<template #append>{{ item.discount_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="挂牌价" label-width="55px">
|
||||
<ElInput v-model="item.listing_price" disabled>
|
||||
<template #append>{{ item.listing_price_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="二手减价" label-width="72px">
|
||||
<ElInput v-model="item.reduction_price" disabled>
|
||||
<template #append>{{ item.reduction_price_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</template>
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="利润" label-width="52px">
|
||||
<ElInput v-model="item.profit" disabled>
|
||||
<template #append>{{ item.profit_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="车架号" label-width="70px">
|
||||
<ElInput v-model.number="item.vin" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="6">
|
||||
<ElFormItem label="合计到岸价" label-width="85px">
|
||||
<ElInput v-model="base.total_cif" disabled>
|
||||
<template #append>{{ base.total_cif_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElFormItem label="合同汇率" label-width="70px">
|
||||
<ElInput v-model="base.contract_rate" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 运输详情 -->
|
||||
<div class="boxshadow">
|
||||
<div class="form-title"><span class="border"></span><span class="title">运输详情</span></div>
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="国内收货港"
|
||||
><ElInput v-model="ship.domestic_port" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="8"
|
||||
><ElFormItem label="贸易条款/清关目的港"
|
||||
><ElInput v-model="ship.terms" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="6"
|
||||
><ElFormItem label="最迟到港时间"
|
||||
><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<!-- 计划收款信息 -->
|
||||
<div class="boxshadow">
|
||||
<div class="form-title"
|
||||
><span class="border"></span><span class="title">计划收款信息</span></div
|
||||
>
|
||||
<div v-for="(p, idx) in payments" :key="idx">
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="汇款国"><ElInput v-model="p.remit_country" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="日期"><ElInput v-model="p.date" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="币种"><ElInput v-model="p.currency" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="账户"><ElInput v-model="p.account" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="汇款人"><ElInput v-model="p.payer" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="金额"><ElInput v-model="p.amount" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 上传合同 -->
|
||||
<div class="boxshadow">
|
||||
<div class="form-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">上传合同</span>
|
||||
</div>
|
||||
<div class="file-preview">
|
||||
<div v-if="contractFile" class="contract-file">
|
||||
<div v-if="isImageFile(contractFile)" class="image-preview">
|
||||
<img
|
||||
:src="contractFile.url"
|
||||
:alt="contractFile.name"
|
||||
class="preview-image"
|
||||
@click="previewImage(contractFile)"
|
||||
/>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ contractFile.name }}</span>
|
||||
<el-button type="text" size="small" @click="previewImage(contractFile)">
|
||||
预览
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="document-preview">
|
||||
<div class="file-icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ contractFile.name }}</span>
|
||||
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
|
||||
下载
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-file">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>暂无合同文件</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PDF 预览弹窗 -->
|
||||
<el-dialog v-model="pdf.visible" title="打印预览" width="80%" top="40px" :close-on-click-modal="false">
|
||||
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px;">
|
||||
<div style="display:flex; justify-content:flex-end; gap: 8px;">
|
||||
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
|
||||
<el-button @click="download">下载</el-button>
|
||||
</div>
|
||||
<iframe
|
||||
v-if="pdf.blobUrl || pdf.url"
|
||||
ref="frameRef"
|
||||
:src="pdf.blobUrl || pdf.url"
|
||||
style="flex:1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { usePdfPreview } from '@/composables/usePdfPreview'
|
||||
const { pdf, frameRef, openWithResponse, download, print, preparePreview } = usePdfPreview()
|
||||
defineOptions({ name: 'AdminFinalShow' })
|
||||
const printCurrent = async () => {
|
||||
const res = await manageService.printStock({ id: stocking_id })
|
||||
openWithResponse(res)
|
||||
preparePreview()
|
||||
}
|
||||
const route = useRoute()
|
||||
const stocking_id = route.query.id
|
||||
const stockingInfo = ref<any>({})
|
||||
|
||||
// 回显视图模型,字段与模板一致
|
||||
const header = reactive({ order_no: '', salesman: '' })
|
||||
const base = reactive({
|
||||
pn_no: '',
|
||||
date: '',
|
||||
close_date: '',
|
||||
channel: '',
|
||||
customer_name: '',
|
||||
client_country: '',
|
||||
total_cif: '',
|
||||
total_cif_currency: '',
|
||||
contract_rate: ''
|
||||
})
|
||||
const items = ref<any[]>([])
|
||||
const ship = reactive({
|
||||
customs: '',
|
||||
plan: '',
|
||||
method: '',
|
||||
domestic_port: '',
|
||||
terms: '',
|
||||
latest_arrival: ''
|
||||
})
|
||||
const payments = ref<any[]>([])
|
||||
|
||||
// 合同文件
|
||||
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
|
||||
|
||||
const toMoneyView = (m: any) => ({
|
||||
amount: m?.amount ?? '',
|
||||
currency: m?.currency ?? ''
|
||||
})
|
||||
|
||||
//获取详情并映射到回显模型
|
||||
const getdetilfn = async () => {
|
||||
const res = await manageService.getStockDetail({ id: stocking_id })
|
||||
stockingInfo.value = res
|
||||
// 头部
|
||||
header.order_no = res?.order_no || res?.pi_no || ''
|
||||
header.salesman = res?.salesperson_snapshot || res?.salesman || res?.salesman_name || res?.user?.name || ''
|
||||
// 基础信息
|
||||
base.pn_no = res?.pi_no || ''
|
||||
base.date = res?.biz_date || ''
|
||||
base.close_date = res?.clearance_country_snapshot || ''
|
||||
base.channel = res?.customer_source_snapshot || res?.customer_source || ''
|
||||
base.customer_name = res?.customer_snapshot || res?.customer_name || ''
|
||||
base.client_country = res?.customer_country_snapshot || res?.customer_country || ''
|
||||
// 订单明细
|
||||
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
|
||||
items.value = list.map((it: any) => {
|
||||
const unit = toMoneyView(it?.unit_price)
|
||||
const total = toMoneyView(it?.total_price)
|
||||
const guide = toMoneyView(it?.official_price)
|
||||
const discount = toMoneyView(it?.discount_price)
|
||||
const listing = toMoneyView(it?.listing_price)
|
||||
const reduction = toMoneyView(it?.reduction_price)
|
||||
const helper = toMoneyView(it?.handling_fee)
|
||||
const fob = toMoneyView(it?.fob_transport_fee)
|
||||
const port = toMoneyView(it?.port_misc_fee)
|
||||
const profit = toMoneyView(it?.profit)
|
||||
return {
|
||||
model: it?.model_snapshot || '',
|
||||
mileage: it?.mileage ?? 0,
|
||||
vin: it?.vin || '',
|
||||
color_out: it?.color_exterior_snapshot || '',
|
||||
color_in: it?.color_interior_snapshot || '',
|
||||
modify: (Array.isArray(it?.modify_list) ? it.modify_list : [])
|
||||
.map((m: any) => m?.option_name_snapshot)
|
||||
.filter(Boolean)
|
||||
.join('、'),
|
||||
modify_fee: it?.retrofit_fee?.amount ?? '',
|
||||
modify_fee_currency: it?.retrofit_fee?.currency ?? '',
|
||||
fob_fee: fob.amount,
|
||||
fob_fee_currency: fob.currency,
|
||||
unit_price: unit.amount,
|
||||
unit_price_currency: unit.currency,
|
||||
total_price: total.amount,
|
||||
total_price_currency: total.currency,
|
||||
guide_price: guide.amount,
|
||||
guide_price_currency: guide.currency,
|
||||
discount: discount.amount,
|
||||
discount_currency: discount.currency,
|
||||
listing_price: listing.amount,
|
||||
listing_price_currency: listing.currency,
|
||||
reduction_price: reduction.amount,
|
||||
reduction_price_currency: reduction.currency,
|
||||
helper_fee: helper.amount,
|
||||
helper_fee_currency: helper.currency,
|
||||
domestic_freight: it?.domestic_freight?.amount ?? '',
|
||||
domestic_freight_currency: it?.domestic_freight?.currency ?? '',
|
||||
port_fee: port.amount,
|
||||
port_fee_currency: port.currency,
|
||||
profit: profit.amount,
|
||||
profit_currency: profit.currency
|
||||
}
|
||||
})
|
||||
// 运输详情
|
||||
ship.customs = res?.declarant_name || ''
|
||||
ship.plan = res?.eta_ship_date || ''
|
||||
ship.method = res?.shipping_method_snapshot || ''
|
||||
ship.domestic_port = res?.domestic_port_snapshot || ''
|
||||
ship.terms = `${res?.trade_term_snapshot || ''} / ${res?.clearance_port_snapshot || ''}`.replace(
|
||||
/^\s*\/\s*$/,
|
||||
''
|
||||
)
|
||||
ship.latest_arrival = res?.latest_arrival_date || ''
|
||||
// 合计与汇率
|
||||
base.total_cif = res?.cif_total_amount?.amount ?? ''
|
||||
base.total_cif_currency = res?.cif_total_amount?.currency || ''
|
||||
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
|
||||
// 计划收款(全部展示)
|
||||
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
|
||||
remit_country: p?.remit_country_name_snapshot || '',
|
||||
date: p?.remit_date || '',
|
||||
currency: p?.currency_name_snapshot || p?.currency_code || '',
|
||||
account: p?.account_name_snapshot || '',
|
||||
payer: p?.remitter_name || '',
|
||||
amount: p?.amount ?? ''
|
||||
}))
|
||||
// 合同文件
|
||||
if (res?.contract) {
|
||||
contractFile.value = {
|
||||
name: '合同文件',
|
||||
url: res.contract_url || res.contract,
|
||||
file_name: res.contract
|
||||
}
|
||||
}
|
||||
}
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 检查文件名
|
||||
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查file_name字段
|
||||
if (file.file_name && file.file_name.startsWith('image/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查URL中的文件扩展名(去除查询参数)
|
||||
if (file.url) {
|
||||
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
|
||||
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 创建图片预览弹窗
|
||||
const imagePreview = document.createElement('div')
|
||||
imagePreview.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const image = document.createElement('img')
|
||||
image.src = file.url
|
||||
image.alt = file.name
|
||||
image.style.cssText = `
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`
|
||||
|
||||
const closeBtn = document.createElement('div')
|
||||
closeBtn.innerHTML = '×'
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
`
|
||||
|
||||
imagePreview.appendChild(image)
|
||||
imagePreview.appendChild(closeBtn)
|
||||
document.body.appendChild(imagePreview)
|
||||
|
||||
// 点击关闭
|
||||
const closePreview = () => {
|
||||
document.body.removeChild(imagePreview)
|
||||
}
|
||||
|
||||
imagePreview.addEventListener('click', closePreview)
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
closePreview()
|
||||
})
|
||||
|
||||
// ESC键关闭
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closePreview()
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = file.url
|
||||
link.download = file.name
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getdetilfn()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stocking-show-page { min-height: 100vh; }
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 12px;
|
||||
color: #333333;
|
||||
.header-left {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.sub {
|
||||
color: #333333;
|
||||
}
|
||||
.mr40 { margin-right: 40px; }
|
||||
}
|
||||
.form-title { font-weight: bold; color: #222; margin-bottom: 15px; }
|
||||
.form-title .title { margin-left: 6px; }
|
||||
.items-wrap { margin-top: 6px; }
|
||||
.item-row { padding: 8px 0; }
|
||||
:deep(.el-input.is-disabled .el-input__inner) { color: var(--el-text-color-regular); }
|
||||
|
||||
.file-preview {
|
||||
padding: 20px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.contract-file {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
object-fit: contain;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.preview-image:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.document-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #409eff;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.no-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-file i {
|
||||
font-size: 48px;
|
||||
color: #ddd;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,670 @@
|
||||
<template>
|
||||
<div class="stocking-show-page">
|
||||
<div class="header boxshadow">
|
||||
<div class="header-left">
|
||||
<div class="title">合塑实业·汽车事业部-备货单(客户下定)</div>
|
||||
<div class="sub">
|
||||
<span class="mr40">备货单编号:{{ header.order_no }}</span>
|
||||
<span>业务员:{{ header.salesman }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ElButton type="primary" plain @click="printCurrent">打印整单</ElButton>
|
||||
</div>
|
||||
|
||||
<!-- 基础信息 -->
|
||||
<div class="boxshadow">
|
||||
<div class="form-title"><span class="border"></span><span class="title">基础信息</span></div>
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="阿里订单号/PI号">
|
||||
<ElInput v-model="base.pn_no" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="日期">
|
||||
<ElInput v-model="base.date" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="清关目的国">
|
||||
<ElInput v-model="base.close_date" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="3">
|
||||
<ElFormItem label="客户来源">
|
||||
<ElInput v-model="base.channel" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="客户名称">
|
||||
<ElInput v-model="base.customer_name" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="客户所在国">
|
||||
<ElInput v-model="base.client_country" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<!-- 订单详情 -->
|
||||
<div class="boxshadow">
|
||||
<div class="form-title"><span class="border"></span><span class="title">订单详情</span></div>
|
||||
<div class="items-wrap">
|
||||
<div class="item-row" v-for="(item, idx) in items" :key="idx">
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="车型" label-width="64px">
|
||||
<ElInput v-model="item.model" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="颜色/外" label-width="72px">
|
||||
<ElInput v-model="item.color_out" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="内饰" label-width="52px">
|
||||
<ElInput v-model="item.color_in" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="改装" label-width="52px">
|
||||
<ElInput v-model="item.modify" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="单价" label-width="52px">
|
||||
<ElInput v-model="item.unit_price" disabled>
|
||||
<template #append>{{ item.unit_price_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="总价" label-width="52px">
|
||||
<ElInput v-model="item.total_price" disabled>
|
||||
<template #append>{{ item.total_price_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="改装费" label-width="60px">
|
||||
<ElInput v-model="item.modify_fee" disabled>
|
||||
<template #append>{{ item.modify_fee_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="FOB段运费" label-width="84px">
|
||||
<ElInput v-model="item.fob_fee" disabled>
|
||||
<template #append>{{ item.fob_fee_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="手续费" label-width="60px">
|
||||
<ElInput v-model="item.helper_fee" disabled>
|
||||
<template #append>{{ item.helper_fee_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="港杂费" label-width="60px">
|
||||
<ElInput v-model="item.port_fee" disabled>
|
||||
<template #append>{{ item.port_fee_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="里程" label-width="52px">
|
||||
<ElInput v-model="item.mileage" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<template v-if="(item.mileage ?? 0) <= 0">
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="官方指导价" label-width="85px">
|
||||
<ElInput v-model="item.guide_price" disabled>
|
||||
<template #append>{{ item.guide_price_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="优惠" label-width="52px">
|
||||
<ElInput v-model="item.discount" disabled>
|
||||
<template #append>{{ item.discount_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</template>
|
||||
<template v-else>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="挂牌价" label-width="55px">
|
||||
<ElInput v-model="item.listing_price" disabled>
|
||||
<template #append>{{ item.listing_price_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="4">
|
||||
<ElFormItem label="二手减价" label-width="72px">
|
||||
<ElInput v-model="item.reduction_price" disabled>
|
||||
<template #append>{{ item.reduction_price_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</template>
|
||||
<ElCol :span="8">
|
||||
<ElFormItem label="利润" label-width="52px">
|
||||
<ElInput v-model="item.profit" disabled>
|
||||
<template #append>{{ item.profit_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="6">
|
||||
<ElFormItem label="合计到岸价" label-width="85px">
|
||||
<ElInput v-model="base.total_cif" disabled>
|
||||
<template #append>{{ base.total_cif_currency }}</template>
|
||||
</ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElFormItem label="合同汇率" label-width="70px">
|
||||
<ElInput v-model="base.contract_rate" disabled />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 运输详情 -->
|
||||
<div class="boxshadow">
|
||||
<div class="form-title"><span class="border"></span><span class="title">运输详情</span></div>
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="清关人"><ElInput v-model="ship.customs" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="预计发送日"><ElInput v-model="ship.plan" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="运输方式"><ElInput v-model="ship.method" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="国内收货港"
|
||||
><ElInput v-model="ship.domestic_port" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="8"
|
||||
><ElFormItem label="贸易条款/清关目的港"
|
||||
><ElInput v-model="ship.terms" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="6"
|
||||
><ElFormItem label="最迟到港时间"
|
||||
><ElInput v-model="ship.latest_arrival" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
|
||||
<!-- 计划收款信息 -->
|
||||
<div class="boxshadow">
|
||||
<div class="form-title"
|
||||
><span class="border"></span><span class="title">计划收款信息</span></div
|
||||
>
|
||||
<div v-for="(p, idx) in payments" :key="idx">
|
||||
<ElRow :gutter="12">
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="汇款国"><ElInput v-model="p.remit_country" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="日期"><ElInput v-model="p.date" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="币种"><ElInput v-model="p.currency" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="账户"><ElInput v-model="p.account" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="汇款人"><ElInput v-model="p.payer" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
<ElCol :span="4"
|
||||
><ElFormItem label="金额"><ElInput v-model="p.amount" disabled /></ElFormItem
|
||||
></ElCol>
|
||||
</ElRow>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 上传合同 -->
|
||||
<div class="boxshadow">
|
||||
<div class="form-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">上传合同</span>
|
||||
</div>
|
||||
<div class="file-preview">
|
||||
<div v-if="contractFile" class="contract-file">
|
||||
<div v-if="isImageFile(contractFile)" class="image-preview">
|
||||
<img
|
||||
:src="contractFile.url"
|
||||
:alt="contractFile.name"
|
||||
class="preview-image"
|
||||
@click="previewImage(contractFile)"
|
||||
/>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ contractFile.name }}</span>
|
||||
<el-button type="text" size="small" @click="previewImage(contractFile)">
|
||||
预览
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="document-preview">
|
||||
<div class="file-icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ contractFile.name }}</span>
|
||||
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
|
||||
下载
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-file">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>暂无合同文件</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- PDF 预览弹窗 -->
|
||||
<el-dialog v-model="pdf.visible" title="打印预览" width="80%" top="40px" :close-on-click-modal="false">
|
||||
<div style="height: 70vh; display: flex; flex-direction: column; gap: 12px;">
|
||||
<div style="display:flex; justify-content:flex-end; gap: 8px;">
|
||||
<!-- <el-button type="primary" @click="print()">打印</el-button> -->
|
||||
<el-button @click="download">下载</el-button>
|
||||
</div>
|
||||
<iframe
|
||||
v-if="pdf.blobUrl || pdf.url"
|
||||
ref="frameRef"
|
||||
:src="pdf.blobUrl || pdf.url"
|
||||
style="flex:1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { usePdfPreview } from '@/composables/usePdfPreview'
|
||||
const { pdf, frameRef, openWithResponse, download, print, preparePreview } = usePdfPreview()
|
||||
defineOptions({ name: 'AdminStockingDetail' })
|
||||
const printCurrent = async () => {
|
||||
const res = await manageService.printStock({ id: stocking_id })
|
||||
openWithResponse(res)
|
||||
preparePreview()
|
||||
}
|
||||
const route = useRoute()
|
||||
const stocking_id = route.query.id
|
||||
const stockingInfo = ref<any>({})
|
||||
|
||||
// 回显视图模型,字段与模板一致
|
||||
const header = reactive({ order_no: '', salesman: '' })
|
||||
const base = reactive({
|
||||
pn_no: '',
|
||||
date: '',
|
||||
close_date: '',
|
||||
channel: '',
|
||||
customer_name: '',
|
||||
client_country: '',
|
||||
total_cif: '',
|
||||
total_cif_currency: '',
|
||||
contract_rate: ''
|
||||
})
|
||||
const items = ref<any[]>([])
|
||||
const ship = reactive({
|
||||
customs: '',
|
||||
plan: '',
|
||||
method: '',
|
||||
domestic_port: '',
|
||||
terms: '',
|
||||
latest_arrival: ''
|
||||
})
|
||||
const payments = ref<any[]>([])
|
||||
|
||||
// 合同文件
|
||||
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
|
||||
|
||||
const toMoneyView = (m: any) => ({
|
||||
amount: m?.amount ?? '',
|
||||
currency: m?.currency ?? ''
|
||||
})
|
||||
|
||||
//获取详情并映射到回显模型
|
||||
const getdetilfn = async () => {
|
||||
const res = await manageService.getStockDetail({ id: stocking_id })
|
||||
stockingInfo.value = res
|
||||
// 头部
|
||||
header.order_no = res?.order_no || res?.pi_no || ''
|
||||
header.salesman = res?.salesperson_snapshot || res?.salesman || res?.salesman_name || res?.user?.name || ''
|
||||
// 基础信息
|
||||
base.pn_no = res?.pi_no || ''
|
||||
base.date = res?.biz_date || ''
|
||||
base.close_date = res?.clearance_country_snapshot || ''
|
||||
base.channel = res?.customer_source_snapshot || res?.customer_source || ''
|
||||
base.customer_name = res?.customer_snapshot || res?.customer_name || ''
|
||||
base.client_country = res?.customer_country_snapshot || res?.customer_country || ''
|
||||
// 订单明细
|
||||
const list = Array.isArray(res?.veh_list) ? res.veh_list : []
|
||||
items.value = list.map((it: any) => {
|
||||
const unit = toMoneyView(it?.unit_price)
|
||||
const total = toMoneyView(it?.total_price)
|
||||
const guide = toMoneyView(it?.official_price)
|
||||
const discount = toMoneyView(it?.discount_price)
|
||||
const helper = toMoneyView(it?.handling_fee)
|
||||
const fob = toMoneyView(it?.fob_transport_fee)
|
||||
const port = toMoneyView(it?.port_misc_fee)
|
||||
const profit = toMoneyView(it?.profit)
|
||||
return {
|
||||
model: it?.model_snapshot || '',
|
||||
mileage: it?.mileage ?? 0,
|
||||
color_out: it?.color_exterior_snapshot || '',
|
||||
color_in: it?.color_interior_snapshot || '',
|
||||
modify: (Array.isArray(it?.modify_list) ? it.modify_list : [])
|
||||
.map((m: any) => m?.option_name_snapshot)
|
||||
.filter(Boolean)
|
||||
.join('、'),
|
||||
modify_fee: it?.retrofit_fee?.amount ?? '',
|
||||
modify_fee_currency: it?.retrofit_fee?.currency ?? '',
|
||||
fob_fee: fob.amount,
|
||||
fob_fee_currency: fob.currency,
|
||||
unit_price: unit.amount,
|
||||
unit_price_currency: unit.currency,
|
||||
total_price: total.amount,
|
||||
total_price_currency: total.currency,
|
||||
guide_price: guide.amount,
|
||||
guide_price_currency: guide.currency,
|
||||
discount: discount.amount,
|
||||
discount_currency: discount.currency,
|
||||
listing_price: it?.listing_price?.amount ?? '',
|
||||
listing_price_currency: it?.listing_price?.currency ?? '',
|
||||
reduction_price: it?.reduction_price?.amount ?? '',
|
||||
reduction_price_currency: it?.reduction_price?.currency ?? '',
|
||||
helper_fee: helper.amount,
|
||||
helper_fee_currency: helper.currency,
|
||||
domestic_freight: it?.domestic_freight?.amount ?? '',
|
||||
domestic_freight_currency: it?.domestic_freight?.currency ?? '',
|
||||
port_fee: port.amount,
|
||||
port_fee_currency: port.currency,
|
||||
profit: profit.amount,
|
||||
profit_currency: profit.currency
|
||||
}
|
||||
})
|
||||
// 运输详情
|
||||
ship.customs = res?.declarant_name || ''
|
||||
ship.plan = res?.eta_ship_date || ''
|
||||
ship.method = res?.shipping_method_snapshot || ''
|
||||
ship.domestic_port = res?.domestic_port_snapshot || ''
|
||||
ship.terms = `${res?.trade_term_snapshot || ''} / ${res?.clearance_port_snapshot || ''}`.replace(
|
||||
/^\s*\/\s*$/,
|
||||
''
|
||||
)
|
||||
ship.latest_arrival = res?.latest_arrival_date || ''
|
||||
// 合计与汇率
|
||||
base.total_cif = res?.cif_total_amount?.amount ?? ''
|
||||
base.total_cif_currency = res?.cif_total_amount?.currency || ''
|
||||
base.contract_rate = res?.contract_exchange_rate || res?.contract_rate || ''
|
||||
// 计划收款(全部展示)
|
||||
payments.value = (Array.isArray(res?.account_list) ? res.account_list : []).map((p: any) => ({
|
||||
remit_country: p?.remit_country_name_snapshot || '',
|
||||
date: p?.remit_date || '',
|
||||
currency: p?.currency_name_snapshot || p?.currency_code || '',
|
||||
account: p?.account_name_snapshot || '',
|
||||
payer: p?.remitter_name || '',
|
||||
amount: p?.amount ?? ''
|
||||
}))
|
||||
// 合同文件
|
||||
if (res?.contract) {
|
||||
contractFile.value = {
|
||||
name: '合同文件',
|
||||
url: res.contract_url || res.contract,
|
||||
file_name: res.contract
|
||||
}
|
||||
}
|
||||
}
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 检查文件名
|
||||
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查file_name字段
|
||||
if (file.file_name && file.file_name.startsWith('image/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查URL中的文件扩展名(去除查询参数)
|
||||
if (file.url) {
|
||||
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
|
||||
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 创建图片预览弹窗
|
||||
const imagePreview = document.createElement('div')
|
||||
imagePreview.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const image = document.createElement('img')
|
||||
image.src = file.url
|
||||
image.alt = file.name
|
||||
image.style.cssText = `
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`
|
||||
|
||||
const closeBtn = document.createElement('div')
|
||||
closeBtn.innerHTML = '×'
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
`
|
||||
|
||||
imagePreview.appendChild(image)
|
||||
imagePreview.appendChild(closeBtn)
|
||||
document.body.appendChild(imagePreview)
|
||||
|
||||
// 点击关闭
|
||||
const closePreview = () => {
|
||||
document.body.removeChild(imagePreview)
|
||||
}
|
||||
|
||||
imagePreview.addEventListener('click', closePreview)
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
closePreview()
|
||||
})
|
||||
|
||||
// ESC键关闭
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closePreview()
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = file.url
|
||||
link.download = file.name
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getdetilfn()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stocking-show-page { min-height: 100vh; }
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 12px;
|
||||
color: #333333;
|
||||
.header-left {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.sub {
|
||||
color: #333333;
|
||||
}
|
||||
.mr40 { margin-right: 40px; }
|
||||
}
|
||||
.form-title { font-weight: bold; color: #222; margin-bottom: 15px; }
|
||||
.form-title .title { margin-left: 6px; }
|
||||
.items-wrap { margin-top: 6px; }
|
||||
.item-row { padding: 8px 0; }
|
||||
:deep(.el-input.is-disabled .el-input__inner) { color: var(--el-text-color-regular); }
|
||||
|
||||
.file-preview {
|
||||
padding: 20px;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.contract-file {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
object-fit: contain;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.preview-image:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.document-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: #409eff;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.no-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-file i {
|
||||
font-size: 48px;
|
||||
color: #ddd;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
<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"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="getList"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getList"><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="order_no" />
|
||||
<ElTableColumn label="阿里订单号/PI号" prop="pi_no" />
|
||||
<ElTableColumn label="客户名称" prop="customer_snapshot" />
|
||||
<ElTableColumn label="客户来源" prop="customer_source_snapshot" />
|
||||
<ElTableColumn label="业务员" prop="salesperson_snapshot" />
|
||||
<ElTableColumn label="日期" prop="created_at" />
|
||||
<!-- <ElTableColumn label="补充" prop="remark" /> -->
|
||||
<ElTableColumn label="状态" prop="approval_status">
|
||||
<template #default="scope">
|
||||
<div v-if="scope.row.appro_status">
|
||||
<div
|
||||
><span :class="statusClass(scope.row.appro_status.status)">{{ scope.row.appro_status.status_desc ? scope.row.appro_status.status_desc : '' }}</span>
|
||||
<span v-if="scope.row.appro_status.name != ''"
|
||||
>【{{ scope.row.appro_status.name }}】</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" fixed="right" min-width="120" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="10" justify="center">
|
||||
<el-link
|
||||
type="warning"
|
||||
underline="never"
|
||||
v-if="scope.row.file_url != null && scope.row.file_url != ''"
|
||||
@click="printfn(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 打印预览
|
||||
</el-link>
|
||||
<el-link
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="scope.row.is_use == 1"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleView1(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看终版
|
||||
</el-link>
|
||||
<el-link type="warning" underline="never" @click="showDialogtimeline(scope.row)">
|
||||
<i class="iconfont-sys"></i> 查看审批
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- PDF 预览弹窗 -->
|
||||
<el-dialog
|
||||
v-model="pdf.visible"
|
||||
title="打印预览"
|
||||
width="80%"
|
||||
top="40px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div style="max-height: 70vh; display: flex; flex-direction: column; gap: 12px">
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px;">
|
||||
<el-button @click="download">下载</el-button>
|
||||
</div>
|
||||
<div
|
||||
v-if="pdf.images.length"
|
||||
style="overflow: auto; border: 1px solid #eee; border-radius: 6px; padding: 8px"
|
||||
>
|
||||
<img
|
||||
v-for="(img, idx) in pdf.images"
|
||||
:key="idx"
|
||||
:src="img"
|
||||
style="display: block; width: 100%; margin-bottom: 8px; background: #fff"
|
||||
/>
|
||||
</div>
|
||||
<iframe
|
||||
v-else-if="pdf.blobUrl || pdf.url"
|
||||
ref="frameRef"
|
||||
:src="pdf.blobUrl || pdf.url"
|
||||
style="flex: 1; width: 100%; border: 1px solid #eee; border-radius: 6px;"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<!-- 订单详情弹窗(使用公共内容组件) -->
|
||||
<el-dialog
|
||||
v-model="orderDialog.visible"
|
||||
:title="orderDialog.title"
|
||||
width="95%"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<OrderDetailContent :orderData="orderDialog.orderData" />
|
||||
<div class="boxshadow">
|
||||
<div class="form-title">
|
||||
<!-- <span class="border"></span> -->
|
||||
<span class="title">合同</span>
|
||||
</div>
|
||||
<div class="file-preview">
|
||||
<div v-if="contractFile" class="contract-file">
|
||||
<div v-if="isImageFile(contractFile)" class="image-preview">
|
||||
<img
|
||||
:src="contractFile.url"
|
||||
:alt="contractFile.name"
|
||||
class="preview-image"
|
||||
@click="previewImage(contractFile)"
|
||||
/>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ contractFile.name }}</span>
|
||||
<el-button type="text" size="small" @click="previewImage(contractFile)">
|
||||
预览
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="document-preview">
|
||||
<div class="file-icon">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<span class="file-name">{{ contractFile.name }}</span>
|
||||
<el-button type="primary" size="small" @click="downloadFile(contractFile)">
|
||||
下载
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-file">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>暂无合同文件</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 8px">
|
||||
<el-button @click="orderDialog.visible = false">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="timelineDialogVisible"
|
||||
title="审批历史"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
class="approval-dialog"
|
||||
>
|
||||
<div class="approval-content">
|
||||
<!-- 头部信息 -->
|
||||
<ApprovalTimeline :records="approvalRecords" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { usePdfPreview } from '@/composables/usePdfPreview'
|
||||
import OrderDetailContent from '@/components/custom/OrderDetailContent.vue'
|
||||
import ApprovalTimeline from '@/components/custom/ApprovalTimeline.vue'
|
||||
import type { OrderData } from '@/composables/useOrderDetailDialog'
|
||||
const timelineDialogVisible = ref(false)
|
||||
const approvalRecords = ref<any[]>([])
|
||||
defineOptions({ name: 'StockingListAdmin' })
|
||||
// import { useAuth } from '@/composables/useAuth'
|
||||
// const { hasAuth } = useAuth()
|
||||
// 订单详情弹窗
|
||||
const orderDialog = reactive({
|
||||
visible: false,
|
||||
title: '订单详情',
|
||||
orderData: {} as OrderData
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const keyword = ref('')
|
||||
const tableRef = ref()
|
||||
const tableData = ref<any[]>([])
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
|
||||
// PDF 预览/打印(公共)
|
||||
const { pdf, frameRef, openWithResponse, download, print, preparePreview, prepareImages } = usePdfPreview()
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
function statusClass(status: any) {
|
||||
if (status === 'rejected') return 'status-lost'
|
||||
if (status === 'approved') return 'status-following'
|
||||
return 'status-dealed'
|
||||
}
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
|
||||
})
|
||||
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
is_final: 0, // 只获取备货单,非终版
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await manageService.getStockList(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
getList()
|
||||
}
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getList()
|
||||
}
|
||||
|
||||
const contractFile = ref<{ name: string; url: string; file_name: string } | null>(null)
|
||||
const handleView = async (row: any) => {
|
||||
try {
|
||||
// 获取订单详情数据
|
||||
const res = await manageService.checkStock({ id: row.id })
|
||||
if (res?.contract) {
|
||||
contractFile.value = {
|
||||
name: '合同文件',
|
||||
url: res.contract_url || res.contract,
|
||||
file_name: res.contract
|
||||
}
|
||||
}
|
||||
if (res) {
|
||||
// 转换数据格式以适配 OrderDetailDialog
|
||||
const orderData: OrderData = res
|
||||
|
||||
orderDialog.orderData = orderData
|
||||
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
|
||||
orderDialog.visible = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取订单详情失败:', error)
|
||||
ElMessage.error('获取订单详情失败')
|
||||
}
|
||||
// router.push({
|
||||
// path: '/admin/stock/show_admin',
|
||||
// query: { id: row.id } // 可带客户名等参数
|
||||
// })
|
||||
// TODO: 跳转或弹窗查看备货单
|
||||
}
|
||||
const handleView1 = async (row: any) => {
|
||||
const res = await manageService.checkStock({ id: row.object_id })
|
||||
if (res?.contract) {
|
||||
contractFile.value = {
|
||||
name: '合同文件',
|
||||
url: res.contract_url || res.contract,
|
||||
file_name: res.contract
|
||||
}
|
||||
}
|
||||
if (res) {
|
||||
// 获取订单详情成功
|
||||
const orderData: OrderData = res
|
||||
orderDialog.orderData = orderData
|
||||
orderDialog.title = '合塑实业-汽车事业部-备货单(客户下定)'
|
||||
orderDialog.visible = true
|
||||
}
|
||||
// router.push({
|
||||
// path: '/admin/stock/final_show_admin',
|
||||
// query: { id: row.object_id } // 可带客户名等参数
|
||||
// })
|
||||
// router.push('/crmpc/stocking/final_show')
|
||||
// TODO: 跳转或弹窗查看备货单
|
||||
}
|
||||
// 判断是否为图片文件
|
||||
const isImageFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 检查文件名
|
||||
if (file.name && /\.(jpg|jpeg|png|gif|webp)$/i.test(file.name)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查file_name字段
|
||||
if (file.file_name && file.file_name.startsWith('image/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查URL中的文件扩展名(去除查询参数)
|
||||
if (file.url) {
|
||||
const urlWithoutParams = file.url.split('?')[0] // 移除查询参数
|
||||
if (/\.(jpg|jpeg|png|gif|webp)$/i.test(urlWithoutParams)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (file: { name: string; url: string; file_name: string }) => {
|
||||
// 创建图片预览弹窗
|
||||
const imagePreview = document.createElement('div')
|
||||
imagePreview.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const image = document.createElement('img')
|
||||
image.src = file.url
|
||||
image.alt = file.name
|
||||
image.style.cssText = `
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`
|
||||
|
||||
const closeBtn = document.createElement('div')
|
||||
closeBtn.innerHTML = '×'
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
color: white;
|
||||
font-size: 30px;
|
||||
cursor: pointer;
|
||||
z-index: 10000;
|
||||
`
|
||||
|
||||
imagePreview.appendChild(image)
|
||||
imagePreview.appendChild(closeBtn)
|
||||
document.body.appendChild(imagePreview)
|
||||
|
||||
// 点击关闭
|
||||
const closePreview = () => {
|
||||
document.body.removeChild(imagePreview)
|
||||
}
|
||||
|
||||
imagePreview.addEventListener('click', closePreview)
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation()
|
||||
closePreview()
|
||||
})
|
||||
|
||||
// ESC键关闭
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closePreview()
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const downloadFile = (file: { name: string; url: string; file_name: string }) => {
|
||||
try {
|
||||
const link = document.createElement('a')
|
||||
link.href = file.url
|
||||
link.download = file.name
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
} catch (error) {
|
||||
console.error('下载失败:', error)
|
||||
ElMessage.error('下载失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
//打印
|
||||
const printfn = (row: any) => {
|
||||
manageService
|
||||
.printStock({ id: row.id })
|
||||
.then((res) => {
|
||||
openWithResponse(res)
|
||||
// 优先转成图片以适配不带插件的环境
|
||||
prepareImages()
|
||||
// 同时保留 blob 预览作为兜底
|
||||
preparePreview()
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
//审批进度
|
||||
const showDialogtimeline = async (row: any) => {
|
||||
// 审批记录(兼容多种字段)
|
||||
const resstockapprovallogs = await manageService.stockapprovaltimeline({
|
||||
id: row.id,
|
||||
flow_type: 'stock_orders'
|
||||
})
|
||||
console.log(resstockapprovallogs)
|
||||
const historyList = resstockapprovallogs
|
||||
if (historyList.length > 0) {
|
||||
approvalRecords.value = historyList.map((h: any) => ({
|
||||
name: h?.initiator_name ? h?.initiator_name : h?.name,
|
||||
role: h?.status_desc,
|
||||
time: h?.time || '',
|
||||
approved: h?.status === 'approved' || h?.status === 'submitted',
|
||||
rejected: h?.status === 'rejected',
|
||||
pending: h?.status != 'approved' && h?.status != 'rejected' && h?.status != 'submitted',
|
||||
comment: h?.comment || h?.remark || ''
|
||||
}))
|
||||
}
|
||||
timelineDialogVisible.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.stocking-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.stocking-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clickable { cursor: pointer; }
|
||||
.status-following {
|
||||
color: #00a43f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-dealed {
|
||||
color: #3471ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-lost {
|
||||
color: #ff0000;
|
||||
font-weight: bold;
|
||||
}
|
||||
.no-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.no-file i {
|
||||
font-size: 48px;
|
||||
color: #ddd;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
<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('system@company_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="ID" prop="id" />
|
||||
<ElTableColumn label="公司名称" prop="name" />
|
||||
<ElTableColumn label="公司标识" prop="abbr" />
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" />
|
||||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('system@company_update')"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteRole(scope.row)"
|
||||
v-if="hasAuth('system@company_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="name">
|
||||
<ElInput v-model="form.name" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="公司标识" prop="abbr">
|
||||
<ElInput v-model="form.abbr" />
|
||||
</ElFormItem>
|
||||
<!-- <ElFormItem label="飞书标识" prop="tenant_key">
|
||||
<ElInput v-model="form.tenant_key" />
|
||||
</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 { systemService } from '@/api/systemApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
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>({
|
||||
name: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
|
||||
abbr: [{ required: true, message: '请输入公司简称', trigger: 'blur' }]
|
||||
// tenant_key: [{ required: true, message: '请输入飞书标识', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const form = reactive<any>({
|
||||
name: '',
|
||||
id: '',
|
||||
abbr: '',
|
||||
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 systemService.getCompanyList({
|
||||
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) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
form.name = row.name
|
||||
form.id = row.id
|
||||
form.status = row.status
|
||||
form.abbr = row.abbr
|
||||
// form.tenant_key = row.tenant_key
|
||||
} else {
|
||||
form.name = ''
|
||||
form.id = ''
|
||||
form.status = 1
|
||||
form.abbr = ''
|
||||
// form.tenant_key = ''
|
||||
}
|
||||
}
|
||||
const deleteRole = (row?: any) => {
|
||||
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await systemService.companyDelete({
|
||||
id: row.id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
getWorkList()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
if (dialogType.value === 'add') {
|
||||
const { message } = await systemService.companyAdd(form)
|
||||
ElMessage.success(message)
|
||||
dialogVisible.value = false
|
||||
getWorkList()
|
||||
} else {
|
||||
const { message } = await systemService.companyEdit(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>
|
||||
@@ -0,0 +1,538 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div v-if="!isMobile" class="dict-layout">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :xs="24" :sm="8" :lg="8">
|
||||
<div class="dict-type-card">
|
||||
<div class="card-header">
|
||||
<ElSelect v-model="category_id" placeholder="请选择字典分类" clearable>
|
||||
<ElOption
|
||||
v-for="item in DictTypeSelect"
|
||||
:key="item.id"
|
||||
:label="item.item_label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<el-input
|
||||
style="margin-left: 8px"
|
||||
v-model="typeKeyword"
|
||||
@keyup.enter="getTypeList"
|
||||
placeholder="请输入关键字"
|
||||
/>
|
||||
<ElButton style="margin-left: 8px; min-width: 70px" @click="getTypeList" v-ripple
|
||||
>搜索</ElButton
|
||||
>
|
||||
<ElButton
|
||||
type="primary"
|
||||
style="margin-left: 8px"
|
||||
@click="showTypeDialog('add')"
|
||||
v-if="hasAuth('system@dictionary_add')"
|
||||
>新增类型</ElButton
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
:data="typeTableData"
|
||||
v-loading="typeLoading"
|
||||
height="calc(100vh - 280px)"
|
||||
@row-click="handleTypeRowClick"
|
||||
:highlight-current-row="true"
|
||||
:row-key="'id'"
|
||||
>
|
||||
<el-table-column prop="dict_code" label="字典编码" />
|
||||
<el-table-column prop="dict_name" label="字典名称" />
|
||||
<el-table-column prop="category" label="字典分类" />
|
||||
<el-table-column prop="description" label="描述" show-overflow-tooltip />
|
||||
<el-table-column fixed="right" label="操作" min-width="140" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
@click.stop="showTypeDialog('edit', scope.row)"
|
||||
v-if="hasAuth('system@dictionary_update')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click.stop="handleTypeDelete(scope.row)"
|
||||
v-if="hasAuth('system@dictionary_destroy')"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无类型" v-show="!typeLoading" />
|
||||
</template>
|
||||
</el-table>
|
||||
<div class="type-pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next"
|
||||
:current-page="typePagination.currentPage"
|
||||
:page-size="typePagination.pageSize"
|
||||
:total="typePagination.total"
|
||||
:page-sizes="[10, 20, 30, 50]"
|
||||
@current-change="handleTypeCurrentChange"
|
||||
@size-change="handleTypeSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="16" :lg="16">
|
||||
<div class="dict-item-card">
|
||||
<div class="card-header">
|
||||
<div class="left">
|
||||
<span v-if="activeType"
|
||||
>当前类型:{{ activeType.dict_name }}({{ activeType.dict_code }})</span
|
||||
>
|
||||
<span v-else class="muted">请选择左侧字典类型</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<ElInput
|
||||
v-model="itemKeyword"
|
||||
placeholder="搜索字典项名称/值"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@keyup.enter="getItemList"
|
||||
/>
|
||||
<ElButton style="margin-left: 8px" @click="getItemList" v-ripple>搜索</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
style="margin-left: 8px"
|
||||
@click="showItemDialog('add')"
|
||||
:disabled="!activeType"
|
||||
v-if="hasAuth('system@dictionary_add')"
|
||||
>新增字典项</ElButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<ArtTable
|
||||
ref="itemTableRef"
|
||||
row-key="id"
|
||||
:loading="itemLoading"
|
||||
:data="itemTableData"
|
||||
:currentPage="itemPagination.currentPage"
|
||||
:pageSize="itemPagination.pageSize"
|
||||
:total="itemPagination.total"
|
||||
@size-change="handleItemSizeChange"
|
||||
@current-change="handleItemCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="ID" prop="id" width="80" />
|
||||
<ElTableColumn label="字典项名称" prop="item_label" min-width="160" />
|
||||
<ElTableColumn
|
||||
label="字典项值"
|
||||
prop="item_value"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<ElTableColumn label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
||||
<!-- <ElTableColumn label="排序" prop="sort" width="100" /> -->
|
||||
<ElTableColumn label="状态" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'primary' : 'info'">{{
|
||||
scope.row.status ? '启用' : '禁用'
|
||||
}}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
|
||||
<ElTableColumn fixed="right" label="操作" min-width="140px" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showItemDialog('edit', scope.row)"
|
||||
v-if="hasAuth('system@dictionary_update')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleItemDelete(scope.row)"
|
||||
v-if="hasAuth('system@dictionary_destroy')"
|
||||
>删除</el-button
|
||||
>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElDialog
|
||||
v-model="typeDialogVisible"
|
||||
:title="typeDialogType === 'add' ? '新增字典类型' : '编辑字典类型'"
|
||||
width="40%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="typeFormRef" :model="typeForm" :rules="typeRules" label-width="100px">
|
||||
<ElFormItem label="字典分类" prop="category_id">
|
||||
<ElSelect v-model="typeForm.category_id" placeholder="请选择字典分类" clearable>
|
||||
<ElOption
|
||||
v-for="item in DictTypeSelect"
|
||||
:key="item.id"
|
||||
:label="item.item_label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="字典编码" prop="dict_code">
|
||||
<ElInput v-model="typeForm.dict_code" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="字典名称" prop="dict_name">
|
||||
<ElInput v-model="typeForm.dict_name" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="description">
|
||||
<ElInput v-model="typeForm.description" type="textarea" :rows="2" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态" prop="status">
|
||||
<ElSwitch v-model="typeForm.status" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="typeDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleTypeSubmit(typeFormRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog
|
||||
v-model="itemDialogVisible"
|
||||
:title="itemDialogType === 'add' ? '新增字典项' : '编辑字典项'"
|
||||
width="50%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="itemFormRef" :model="itemForm" :rules="itemRules" label-width="100px">
|
||||
<ElFormItem label="所属类型">
|
||||
<ElInput
|
||||
:model-value="activeType ? `${activeType.dict_name}(${activeType.dict_code})` : ''"
|
||||
disabled
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="字典项名称" prop="item_label">
|
||||
<ElInput v-model="itemForm.item_label" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="字典项值" prop="item_value">
|
||||
<ElInput v-model="itemForm.item_value" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="itemForm.sort" :min="0" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注" prop="remark">
|
||||
<ElInput v-model="itemForm.remark" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态" prop="status">
|
||||
<ElSwitch v-model="itemForm.status" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="itemDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleItemSubmit(itemFormRef)">提交</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 { systemService } from '@/api/systemApi'
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'Dictionary' })
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = computed(() => width.value < 500)
|
||||
|
||||
// 左侧 类型
|
||||
const typeLoading = ref(false)
|
||||
const typeKeyword = ref<string>('')
|
||||
const category_id = ref<string>('')
|
||||
const typeTableData = ref<any[]>([])
|
||||
const typePagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
const activeType = ref<any | null>(null)
|
||||
|
||||
const typeFormRef = ref<FormInstance>()
|
||||
const typeDialogVisible = ref(false)
|
||||
const typeDialogType = ref<'add' | 'edit'>('add')
|
||||
const typeForm = reactive<any>({ id: '', code: '', name: '', status: 1 })
|
||||
const typeRules = reactive<FormRules>({
|
||||
dict_code: [{ required: true, message: '请输入字典编码', trigger: 'blur' }],
|
||||
dict_name: [{ required: true, message: '请输入字典名称', trigger: 'blur' }],
|
||||
category_id: [{ required: true, message: '请选择字典分类', trigger: 'blur' }]
|
||||
})
|
||||
const DictTypeSelect = ref<any[]>([])
|
||||
const getDictTypeSelect = async () => {
|
||||
const records = await commonService.getDictTypeSelect()
|
||||
DictTypeSelect.value = records.sysDictItem
|
||||
}
|
||||
|
||||
const getTypeList = async () => {
|
||||
typeLoading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = typePagination
|
||||
const { records, current, size, total } = await systemService.getDictTypeList({
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: typeKeyword.value,
|
||||
category_id: category_id.value
|
||||
})
|
||||
typeTableData.value = records
|
||||
Object.assign(typePagination, { currentPage: current, pageSize: size, total })
|
||||
if (!activeType.value && records && records.length) {
|
||||
activeType.value = records[0]
|
||||
await getItemList()
|
||||
}
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleTypeRowClick = async (row: any) => {
|
||||
activeType.value = row
|
||||
itemPagination.currentPage = 1
|
||||
await getItemList()
|
||||
}
|
||||
const handleTypeSizeChange = (val: number) => {
|
||||
typePagination.pageSize = val
|
||||
getTypeList()
|
||||
}
|
||||
const handleTypeCurrentChange = (val: number) => {
|
||||
typePagination.currentPage = val
|
||||
getTypeList()
|
||||
}
|
||||
|
||||
const showTypeDialog = (type: 'add' | 'edit', row?: any) => {
|
||||
typeDialogVisible.value = true
|
||||
typeDialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(typeForm, {
|
||||
id: row.id,
|
||||
dict_code: row.dict_code,
|
||||
dict_name: row.dict_name,
|
||||
description: row.description,
|
||||
category_id: row.category_id,
|
||||
status: row.status ?? 1
|
||||
})
|
||||
} else {
|
||||
Object.assign(typeForm, {
|
||||
id: '',
|
||||
dict_code: '',
|
||||
dict_name: '',
|
||||
description: '',
|
||||
status: 1,
|
||||
category_id: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleTypeDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该字典类型吗?删除后其字典项也可能被清理。', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await systemService.dictTypeDelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
if (activeType.value && activeType.value.id === row.id) {
|
||||
activeType.value = null
|
||||
itemTableData.value = []
|
||||
itemPagination.total = 0
|
||||
}
|
||||
getTypeList()
|
||||
})
|
||||
}
|
||||
const handleTypeSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (typeDialogType.value === 'add') {
|
||||
const { message } = await systemService.dictTypeAdd({
|
||||
dict_code: typeForm.dict_code,
|
||||
dict_name: typeForm.dict_name,
|
||||
description: typeForm.description,
|
||||
status: typeForm.status,
|
||||
category_id: typeForm.category_id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await systemService.dictTypeEdit({
|
||||
id: typeForm.id,
|
||||
dict_code: typeForm.dict_code,
|
||||
dict_name: typeForm.dict_name,
|
||||
description: typeForm.description,
|
||||
category_id: typeForm.category_id,
|
||||
status: typeForm.status
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
typeDialogVisible.value = false
|
||||
getTypeList()
|
||||
})
|
||||
}
|
||||
|
||||
// 右侧 字典项
|
||||
const itemLoading = ref(false)
|
||||
const itemKeyword = ref<string>('')
|
||||
const itemTableData = ref<any[]>([])
|
||||
const itemTableRef = ref()
|
||||
const itemPagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
|
||||
const itemFormRef = ref<FormInstance>()
|
||||
const itemDialogVisible = ref(false)
|
||||
const itemDialogType = ref<'add' | 'edit'>('add')
|
||||
const itemForm = reactive<any>({
|
||||
id: '',
|
||||
item_label: '',
|
||||
item_value: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
const itemRules = reactive<FormRules>({
|
||||
item_label: [{ required: true, message: '请输入字典项', trigger: 'blur' }],
|
||||
item_value: [{ required: true, message: '请输入描述', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const getItemList = async () => {
|
||||
if (!activeType.value) return
|
||||
itemLoading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = itemPagination
|
||||
const { records, current, size, total } = await systemService.getDictItemList({
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: itemKeyword.value,
|
||||
id: activeType.value.id
|
||||
})
|
||||
itemTableData.value = records
|
||||
Object.assign(itemPagination, { currentPage: current, pageSize: size, total })
|
||||
} finally {
|
||||
itemLoading.value = false
|
||||
}
|
||||
}
|
||||
const handleItemSizeChange = (val: number) => {
|
||||
itemPagination.pageSize = val
|
||||
getItemList()
|
||||
}
|
||||
const handleItemCurrentChange = (val: number) => {
|
||||
itemPagination.currentPage = val
|
||||
getItemList()
|
||||
}
|
||||
const showItemDialog = (type: 'add' | 'edit', row?: any) => {
|
||||
itemDialogVisible.value = true
|
||||
itemDialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(itemForm, {
|
||||
id: row.id,
|
||||
item_label: row.item_label,
|
||||
item_value: row.item_value,
|
||||
sort: row.sort ?? 0,
|
||||
status: row.status ?? 1,
|
||||
remark: row.remark || ''
|
||||
})
|
||||
} else {
|
||||
Object.assign(itemForm, {
|
||||
id: '',
|
||||
item_label: '',
|
||||
item_value: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleItemDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该字典项吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await systemService.dictItemDelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getItemList()
|
||||
})
|
||||
}
|
||||
const handleItemSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl || !activeType.value) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (itemDialogType.value === 'add') {
|
||||
const { message } = await systemService.dictItemAdd({
|
||||
dict_id: activeType.value.id,
|
||||
item_label: itemForm.item_label,
|
||||
item_value: itemForm.item_value,
|
||||
sort: itemForm.sort,
|
||||
status: itemForm.status,
|
||||
remark: itemForm.remark
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await systemService.dictItemEdit({
|
||||
id: itemForm.id,
|
||||
dict_id: activeType.value.id,
|
||||
item_label: itemForm.item_label,
|
||||
item_value: itemForm.item_value,
|
||||
sort: itemForm.sort,
|
||||
status: itemForm.status,
|
||||
remark: itemForm.remark
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
itemDialogVisible.value = false
|
||||
getItemList()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getDictTypeSelect()
|
||||
await getTypeList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.dict-layout {
|
||||
.dict-type-card,
|
||||
.dict-item-card {
|
||||
background: var(--el-bg-color);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
.left {
|
||||
font-weight: 500;
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.muted {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
.type-pagination {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div v-if="!isMobile">
|
||||
<ElRow>
|
||||
<!-- <ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput placeholder="名称"></ElInput>
|
||||
</ElCol> -->
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<!-- <ElButton v-ripple>搜索</ElButton> -->
|
||||
<ElButton @click="showDialog('add')" v-if="hasAuth('system@feishu_app_add')" v-ripple
|
||||
>新增</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="ID" prop="id" width="80" />
|
||||
<ElTableColumn label="公司" prop="corp_id" width="140">
|
||||
<template #default="scope">
|
||||
{{ scope.row.corpInfo.name }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="系统" prop="system_id" width="140">
|
||||
<template #default="scope">
|
||||
{{ scope.row.systemInfo.name }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="名称" prop="name" width="160" />
|
||||
<ElTableColumn label="应用ID" prop="app_id" width="180" show-overflow-tooltip />
|
||||
<ElTableColumn label="基础域" prop="base_uri" width="220" show-overflow-tooltip />
|
||||
<ElTableColumn label="授权地址" prop="r_url" width="260" show-overflow-tooltip />
|
||||
<ElTableColumn label="授权范围" prop="scopes" width="240" show-overflow-tooltip />
|
||||
<ElTableColumn label="携带参数" prop="state" width="140" show-overflow-tooltip />
|
||||
<ElTableColumn label="排序" prop="sort" width="90" />
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" width="180" />
|
||||
<ElTableColumn fixed="right" label="操作" width="180px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('system@feishu_app_update')"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteRole(scope.row)"
|
||||
v-if="hasAuth('system@feishu_app_destroy')"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增' : '编辑'"
|
||||
align-center
|
||||
>
|
||||
<ElForm
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
style="padding: 5px 20px"
|
||||
label-width="100px"
|
||||
>
|
||||
<ElFormItem label="公司" prop="corp_id">
|
||||
<ElSelect v-model="form.corp_id" placeholder="请选择公司" filterable style="width: 100%">
|
||||
<ElOption v-for="item in companyList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="系统" prop="system_id">
|
||||
<ElSelect v-model="form.system_id" placeholder="请选择系统" filterable style="width: 100%">
|
||||
<ElOption v-for="item in systemsList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="请输入名称" />
|
||||
</ElFormItem>
|
||||
<!-- <ElFormItem label="企业标识" prop="tenant_key">
|
||||
<ElInput v-model="form.tenant_key" placeholder="请输入企业标识" />
|
||||
</ElFormItem> -->
|
||||
<ElFormItem label="应用ID" prop="app_id">
|
||||
<ElInput v-model="form.app_id" placeholder="请输入应用ID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="应用秘钥" prop="app_secret">
|
||||
<ElInput v-model="form.app_secret" placeholder="请输入应用秘钥" show-password />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="基础域" prop="base_uri">
|
||||
<ElInput v-model="form.base_uri" placeholder="请输入基础域" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="授权地址" prop="r_url">
|
||||
<ElInput v-model="form.r_url" placeholder="请输入授权地址" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="授权范围" prop="scopes">
|
||||
<ElInput v-model="form.scopes" type="textarea" :rows="3" placeholder="请输入授权范围" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="携带参数" prop="state">
|
||||
<ElInput v-model="form.state" placeholder="请输入携带参数" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="form.sort" :min="0" placeholder="请输入排序" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态" prop="status">
|
||||
<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 { systemService } from '@/api/systemApi'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'FeishuApp' })
|
||||
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>({
|
||||
corp_id: [{ required: true, message: '请选择公司', trigger: 'change' }],
|
||||
system_id: [{ required: true, message: '请选择系统', trigger: 'change' }],
|
||||
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
||||
// app_id: [{ required: true, message: '请输入应用ID', trigger: 'blur' }],
|
||||
// app_secret: [{ required: true, message: '请输入应用秘钥', trigger: 'blur' }],
|
||||
// base_uri: [{ required: true, message: '请输入基础域', trigger: 'blur' }],
|
||||
// r_url: [{ required: true, message: '请输入授权地址', trigger: 'blur' }],
|
||||
// scopes: [{ required: true, message: '请输入授权范围', trigger: 'blur' }],
|
||||
// state: [{ required: true, message: '请输入携带参数', trigger: 'blur' }],
|
||||
// sort: [{ required: true, message: '请输入排序', trigger: 'blur' }],
|
||||
// status: [{ required: true, message: '请选择状态', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
corp_id: '',
|
||||
system_id: '',
|
||||
name: '',
|
||||
// tenant_key: '',
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
base_uri: '',
|
||||
r_url: '',
|
||||
scopes: '',
|
||||
state: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
id: ''
|
||||
})
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const companyList = ref<any[]>([])
|
||||
const systemsList = ref<any[]>([])
|
||||
const getcompanyList = async () => {
|
||||
const response = await UserService.getCompanyList()
|
||||
const list = (response as any)?.data ?? (response as any)?.records ?? response
|
||||
companyList.value = Array.isArray(list) ? list : []
|
||||
}
|
||||
const getsystemsList = async () => {
|
||||
const response = await UserService.getsystemsList()
|
||||
const list = (response as any)?.data ?? (response as any)?.records ?? response
|
||||
systemsList.value = Array.isArray(list) ? list : []
|
||||
}
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
getWorkList()
|
||||
getcompanyList()
|
||||
getsystemsList()
|
||||
})
|
||||
// 获取列表数据
|
||||
const getWorkList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
|
||||
const { records, current, size, total } = await systemService.getFeishuAppList({
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
})
|
||||
tableData.value = records.map((item: any, index: number) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getWorkList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getWorkList()
|
||||
}
|
||||
|
||||
const dialogType = ref('add')
|
||||
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
// 编辑模式,填充所有字段
|
||||
Object.assign(form, {
|
||||
corp_id: row.corp_id || '',
|
||||
system_id: row.system_id || '',
|
||||
name: row.name || '',
|
||||
// tenant_key: row.tenant_key || '',
|
||||
app_id: row.app_id || '',
|
||||
app_secret: row.app_secret || '',
|
||||
base_uri: row.base_uri || '',
|
||||
r_url: row.r_url || '',
|
||||
scopes: row.scopes || '',
|
||||
state: row.state || '',
|
||||
sort: row.sort || 0,
|
||||
status: row.status !== undefined ? row.status : 1,
|
||||
id: row.id || ''
|
||||
})
|
||||
} else {
|
||||
// 新增模式,重置所有字段
|
||||
Object.assign(form, {
|
||||
corp_id: '',
|
||||
system_id: '',
|
||||
name: '',
|
||||
// tenant_key: '',
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
base_uri: '',
|
||||
r_url: '',
|
||||
scopes: '',
|
||||
state: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
id: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
const deleteRole = (row?: any) => {
|
||||
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await systemService.feishuAppDelete({
|
||||
id: row.id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
getWorkList()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
const { message } = await systemService.feishuAppAdd({
|
||||
corp_id: form.corp_id,
|
||||
system_id: form.system_id,
|
||||
name: form.name,
|
||||
app_id: form.app_id,
|
||||
app_secret: form.app_secret,
|
||||
base_uri: form.base_uri,
|
||||
r_url: form.r_url,
|
||||
scopes: form.scopes,
|
||||
state: form.state,
|
||||
sort: form.sort,
|
||||
status: form.status
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await systemService.feishuAppEdit({
|
||||
id: form.id,
|
||||
corp_id: form.corp_id,
|
||||
system_id: form.system_id,
|
||||
name: form.name,
|
||||
app_id: form.app_id,
|
||||
app_secret: form.app_secret,
|
||||
base_uri: form.base_uri,
|
||||
r_url: form.r_url,
|
||||
scopes: form.scopes,
|
||||
state: form.state,
|
||||
sort: form.sort,
|
||||
status: form.status
|
||||
})
|
||||
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>
|
||||
@@ -0,0 +1,773 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="menu-page" id="table-full-screen">
|
||||
<!-- 搜索栏 -->
|
||||
<ArtSearchBar
|
||||
v-model:filter="formFilters"
|
||||
:items="formItems"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
></ArtSearchBar>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader :showZebra="false" v-model:columns="columnChecks" @refresh="handleRefresh">
|
||||
<template #left>
|
||||
<el-segmented
|
||||
v-model="yearVal"
|
||||
size="large"
|
||||
:options="segmentedOptions"
|
||||
@change="searchArticleByYear"
|
||||
>
|
||||
<!-- <template #default="scope">
|
||||
<div class="flex flex-col items-center gap-2 p-2">
|
||||
<div>{{ scope.item.label }}</div>
|
||||
</div>
|
||||
</template> -->
|
||||
</el-segmented>
|
||||
<!-- 按钮权限:后端控制模式,使用自定义指令 -->
|
||||
<!-- <ElButton
|
||||
v-auth="'system@menu_add'"
|
||||
@click="showModel('menu', null)"
|
||||
v-ripple
|
||||
style="margin-left: 20px"
|
||||
>
|
||||
添加菜单
|
||||
</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</ElButton> -->
|
||||
<!-- 按钮权限:前端控制模式,使用 hasAuth 方法 -->
|
||||
<!-- <ElButton v-if="hasAuth('B_CODE1')" @click="showModel('menu', null, true)" v-ripple>
|
||||
添加菜单
|
||||
</ElButton> -->
|
||||
</template>
|
||||
<template #right>
|
||||
<div class="btnadds" @click="showModel('menu', null)" v-auth="'system@menu_add'">
|
||||
<i class="iconfont-sys"></i>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
rowKey="path"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
:data="filteredTableData"
|
||||
:marginTop="10"
|
||||
:stripe="false"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog :title="dialogTitle" v-model="dialogVisible" width="700px" align-center>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="85px">
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="资源类型" prop="type">
|
||||
<el-select v-model="form.type">
|
||||
<el-option label="目录" value="directory" />
|
||||
<el-option label="菜单" value="menu" />
|
||||
<el-option label="权限" value="permission" />
|
||||
</el-select>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="资源名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="资源名称"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="权限标识" prop="code">
|
||||
<ElInput v-model="form.code" placeholder="权限标识"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="图标" prop="icon">
|
||||
<ArtIconSelector v-model="form.icon" :iconType="iconType" width="100%" />
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20" v-if="form.type !== 'permission'">
|
||||
<ElCol :span="12" v-if="form.type !== 'permission'">
|
||||
<ElFormItem label="路由地址" prop="url">
|
||||
<ElInput v-model="form.url" placeholder="路由地址"></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="打开方式" prop="open_type">
|
||||
<ElSelect v-model="form.open_type" class="account-select">
|
||||
<el-option label="内部" value="internal" />
|
||||
<el-option label="外部" value="external" />
|
||||
<el-option label="嵌入" value="iframe" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20" v-if="form.type !== 'permission'">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="资源排序" prop="sort" style="width: 100%">
|
||||
<ElInputNumber
|
||||
v-model="form.sort"
|
||||
style="width: 100%"
|
||||
@change="handleChange"
|
||||
:min="1"
|
||||
controls-position="right"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="外部链接" prop="link">
|
||||
<ElInput
|
||||
v-model="form.link"
|
||||
placeholder="外部链接/内嵌地址(https://www.baidu.com)"
|
||||
></ElInput>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="系统" prop="system_id">
|
||||
<ElSelect v-model="form.system_id" class="account-select">
|
||||
<ElOption
|
||||
v-for="account in systemsList"
|
||||
:key="account.id"
|
||||
:label="account.name"
|
||||
:value="account.id"
|
||||
>
|
||||
<span>{{ account.name }}</span>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="12">
|
||||
<ElFormItem label="父级" prop="parent_id">
|
||||
<el-tree-select
|
||||
v-model="form.parent_id"
|
||||
:data="parentList"
|
||||
:props="{
|
||||
value: 'id',
|
||||
label: 'name',
|
||||
children: 'children'
|
||||
}"
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
style="width: 240px"
|
||||
/>
|
||||
<!-- <ElSelect v-model="form.parent_id" class="account-select">
|
||||
<ElOption
|
||||
v-for="account in parentList"
|
||||
:key="account.id"
|
||||
:label="account.name"
|
||||
:value="account.id"
|
||||
>
|
||||
<span>{{ account.name }}</span>
|
||||
</ElOption>
|
||||
</ElSelect> -->
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElRow :gutter="20" v-if="form.type !== 'permission'">
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否启用" prop="status">
|
||||
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="页面缓存" prop="keep_alive">
|
||||
<ElSwitch
|
||||
v-model="form.keep_alive"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否隐藏" prop="is_hide">
|
||||
<ElSwitch
|
||||
v-model="form.is_hide"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="标签页是否隐藏" prop="is_hide_tab" label-width="120px">
|
||||
<ElSwitch
|
||||
v-model="form.is_hide_tab"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<!-- <ElCol :span="5">
|
||||
<ElFormItem label="是否内嵌" prop="isMenu">
|
||||
<ElSwitch v-model="form.isIframe"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol> -->
|
||||
</ElRow>
|
||||
<ElRow :gutter="20" v-if="form.type !== 'permission'">
|
||||
<ElCol :span="5">
|
||||
<ElFormItem label="是否首页" prop="is_home">
|
||||
<ElSwitch v-model="form.is_home" :active-value="1" :inactive-value="0"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
<ElCol :span="10">
|
||||
<ElFormItem label="不需要认证" prop="no_auth" label-width="120px">
|
||||
<ElSwitch v-model="form.no_auth" :active-value="1" :inactive-value="0"></ElSwitch>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取 消</ElButton>
|
||||
<ElButton type="primary" @click="submitForm()">确 定</ElButton>
|
||||
</span>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</ElCard>
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { IconTypeEnum } from '@/enums/appEnum'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
import ArtButtonTable from '@/components/core/forms/ArtButtonTable.vue'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { ElPopover, ElButton } from 'element-plus'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { SearchFormItem } from '@/types'
|
||||
import { systemService } from '@/api/systemApi'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
const yearVal = ref<any>('')
|
||||
defineOptions({ name: 'Menus' })
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
|
||||
// const { menuList } = storeToRefs(useMenuStore())
|
||||
const menuList = ref<any[]>([])
|
||||
|
||||
const loading = ref(false)
|
||||
const systemsListtab = ref<any>([])
|
||||
const segmentedOptions = computed(() =>
|
||||
Array.isArray(systemsListtab.value)
|
||||
? systemsListtab.value.map((item: any) => ({ label: item.name, value: item.id }))
|
||||
: []
|
||||
)
|
||||
const getsystemsListtab = async () => {
|
||||
const response = await UserService.getsystemsList()
|
||||
const list = (response as any)?.data?.records ?? (response as any)?.records ?? response ?? []
|
||||
systemsListtab.value = Array.isArray(list) ? list : []
|
||||
if (!yearVal.value && systemsListtab.value.length > 0) {
|
||||
yearVal.value = systemsListtab.value[0].id
|
||||
}
|
||||
}
|
||||
//系统
|
||||
const systemsList = ref<any>()
|
||||
const getsystemsList = async () => {
|
||||
const response = await UserService.getsystemsList()
|
||||
systemsList.value = response || []
|
||||
}
|
||||
//父级
|
||||
const parentList = ref<any>()
|
||||
const getparentList = async () => {
|
||||
const response = await systemService.getMenuList()
|
||||
parentList.value = response || []
|
||||
}
|
||||
// 定义表单搜索初始值
|
||||
const initialSearchState = {
|
||||
name: '',
|
||||
route: ''
|
||||
}
|
||||
|
||||
// 响应式表单数据
|
||||
const formFilters = reactive({ ...initialSearchState })
|
||||
|
||||
// 增加实际应用的搜索条件状态
|
||||
const appliedFilters = reactive({ ...initialSearchState })
|
||||
|
||||
// 重置表单
|
||||
const handleReset = () => {
|
||||
Object.assign(formFilters, { ...initialSearchState })
|
||||
Object.assign(appliedFilters, { ...initialSearchState })
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
// 将当前输入的筛选条件应用到实际搜索
|
||||
Object.assign(appliedFilters, { ...formFilters })
|
||||
getTableData()
|
||||
}
|
||||
|
||||
// 表单配置项
|
||||
const formItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '菜单名称',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '路由地址',
|
||||
prop: 'route',
|
||||
type: 'input',
|
||||
config: {
|
||||
clearable: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 构建菜单类型标签
|
||||
const buildMenuTypeTag = (row: AppRouteRecord) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
return 'info'
|
||||
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||
return 'success'
|
||||
} else if (row.path) {
|
||||
return 'primary'
|
||||
} else if (row.meta?.link) {
|
||||
return 'warning'
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单类型文本
|
||||
const buildMenuTypeText = (row: AppRouteRecord) => {
|
||||
if (row.children && row.children.length > 0) {
|
||||
return '目录'
|
||||
} else if (row.meta?.link && row.meta?.isIframe) {
|
||||
return '内嵌'
|
||||
} else if (row.path) {
|
||||
return '菜单'
|
||||
} else if (row.meta?.link) {
|
||||
return '外链'
|
||||
}
|
||||
}
|
||||
|
||||
// 动态列配置
|
||||
const { columnChecks, columns } = useCheckedColumns(() => [
|
||||
{
|
||||
prop: 'meta.title',
|
||||
label: '菜单名称',
|
||||
minWidth: 120,
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return formatMenuTitle(row.meta?.title)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'type',
|
||||
label: '菜单类型',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(ElTag, { type: buildMenuTypeTag(row) }, () => buildMenuTypeText(row))
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'path',
|
||||
label: '路由',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return row.meta?.link || row.path || ''
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'meta.authList',
|
||||
label: '可操作权限',
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h(
|
||||
'div',
|
||||
{},
|
||||
row.meta.authList?.map((item: { title: string; auth_mark: string }, index: number) => {
|
||||
return h(
|
||||
ElPopover,
|
||||
{
|
||||
placement: 'top-start',
|
||||
title: '操作',
|
||||
width: 200,
|
||||
trigger: 'click',
|
||||
key: index
|
||||
},
|
||||
{
|
||||
default: () =>
|
||||
h('div', { style: 'margin: 0; text-align: right' }, [
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'primary',
|
||||
onClick: () => showModel('button', item, false, row)
|
||||
},
|
||||
{ default: () => '编辑' }
|
||||
),
|
||||
h(
|
||||
ElButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'danger',
|
||||
onClick: () => deleteAuth(item)
|
||||
},
|
||||
{ default: () => '删除' }
|
||||
)
|
||||
]),
|
||||
reference: () => h(ElButton, { class: 'small-btn' }, { default: () => item.title })
|
||||
}
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'date',
|
||||
label: '编辑时间',
|
||||
formatter: () => '2022-3-12 12:00:00'
|
||||
},
|
||||
{
|
||||
prop: 'status',
|
||||
label: '隐藏菜单',
|
||||
formatter: (row) => {
|
||||
return h(ElTag, { type: row.meta.isHide ? 'danger' : 'info' }, () =>
|
||||
row.meta.isHide ? '是' : '否'
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 180,
|
||||
formatter: (row: AppRouteRecord) => {
|
||||
return h('div', [
|
||||
// 后端模式权限标识
|
||||
hasAuth('system@menu_add') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'add',
|
||||
onClick: () => showModel('menu', row, true)
|
||||
}),
|
||||
hasAuth('system@menu_update') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'edit',
|
||||
onClick: () => showDialog('edit', row)
|
||||
}),
|
||||
hasAuth('system@menu_destroy') &&
|
||||
h(ArtButtonTable, {
|
||||
type: 'delete',
|
||||
onClick: () => deleteMenu(row)
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const handleRefresh = () => {
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const form = reactive({
|
||||
// 菜单
|
||||
system_id: '',
|
||||
type: 'menu', // 资源类型
|
||||
parent_id: '',
|
||||
name: '',
|
||||
url: '',
|
||||
code: '',
|
||||
icon: '',
|
||||
status: 1,
|
||||
sort: 1,
|
||||
is_home: 0,
|
||||
keep_alive: 1,
|
||||
is_hide: 0,
|
||||
is_hide_tab: 0,
|
||||
no_auth: 0,
|
||||
link: '',
|
||||
open_type: 'internal'
|
||||
})
|
||||
const iconType = ref(IconTypeEnum.UNICODE)
|
||||
|
||||
// const labelPosition = ref('menu')
|
||||
const rules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入菜单名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
type: [{ required: true, message: '请选择资源类型', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '输入权限标识', trigger: 'blur' }]
|
||||
})
|
||||
const tableData = ref<AppRouteRecord[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getsystemsListtab()
|
||||
getTableData()
|
||||
})
|
||||
const searchArticleByYear = () => {
|
||||
getTableData()
|
||||
}
|
||||
// const handleSegmentedClick = (item: any) => {
|
||||
// // 若点击的是当前项也强制刷新
|
||||
// yearVal.value = item?.value ?? yearVal.value
|
||||
// getTableData()
|
||||
// }
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
// 清空旧数据,避免视觉上出现追加
|
||||
menuList.value = []
|
||||
tableData.value = []
|
||||
// 并发安全:本次请求标识
|
||||
requestCounter += 1
|
||||
const reqId = requestCounter
|
||||
const params: any = {
|
||||
system_id: yearVal.value || '1'
|
||||
}
|
||||
try {
|
||||
const res = await systemService.getMenutreeList(params)
|
||||
// 若在等待过程中又发起了新请求,则丢弃本次结果
|
||||
if (reqId !== requestCounter) return
|
||||
menuList.value = Array.isArray(res) ? res : []
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
menuList.value = []
|
||||
}
|
||||
// console.log(res)
|
||||
// menuList.value = data
|
||||
|
||||
// 基于 appliedFilters 在这里进行过滤,并将结果赋值给 tableData
|
||||
const searchName = appliedFilters.name?.toLowerCase().trim() || ''
|
||||
const searchRoute = appliedFilters.route?.toLowerCase().trim() || ''
|
||||
|
||||
const filterMenus = (items: AppRouteRecord[]): AppRouteRecord[] => {
|
||||
return items.reduce((acc: AppRouteRecord[], item: AppRouteRecord) => {
|
||||
// 浅拷贝节点,避免直接修改原数据
|
||||
const copy: AppRouteRecord = { ...item }
|
||||
if (copy.children && copy.children.length > 0) {
|
||||
copy.children = filterMenus(copy.children)
|
||||
}
|
||||
|
||||
const menuTitle = formatMenuTitle(copy.meta?.title || '').toLowerCase()
|
||||
const menuPath = (copy.path || '').toLowerCase()
|
||||
const nameMatch = !searchName || menuTitle.includes(searchName)
|
||||
const routeMatch = !searchRoute || menuPath.includes(searchRoute)
|
||||
const hasChildren = Array.isArray(copy.children) && copy.children.length > 0
|
||||
|
||||
if (hasChildren || (nameMatch && routeMatch)) {
|
||||
acc.push(copy)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
}
|
||||
|
||||
tableData.value = filterMenus(menuList.value)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
// 过滤后的表格数据改为来源于 getTableData 的结果
|
||||
const filteredTableData = computed(() => tableData.value)
|
||||
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogTitle = computed(() => {
|
||||
// const type = labelPosition.value === 'menu' ? '菜单' : '权限'
|
||||
return isEdit.value ? `编辑资源` : `新建资源`
|
||||
})
|
||||
|
||||
const showDialog = (type: string, row: AppRouteRecord) => {
|
||||
showModel('menu', row)
|
||||
}
|
||||
|
||||
const handleChange = () => {}
|
||||
|
||||
const submitForm = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
const payload = { ...form, id: itemid.value }
|
||||
await systemService.menuEdit(payload as any)
|
||||
} else {
|
||||
await systemService.menuAdd(form)
|
||||
}
|
||||
|
||||
ElMessage.success(`${isEdit.value ? '编辑' : '新增'}成功`)
|
||||
dialogVisible.value = false
|
||||
// 刷新表格数据
|
||||
getTableData()
|
||||
} catch {
|
||||
ElMessage.error(`${isEdit.value ? '编辑' : '新增'}失败`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
const itemid = ref('')
|
||||
const showModel = (type: string, row?: any, padd: boolean = false, data?: any) => {
|
||||
getsystemsList()
|
||||
getparentList()
|
||||
dialogVisible.value = true
|
||||
// labelPosition.value = type
|
||||
isEdit.value = false
|
||||
// lockMenuType.value = lock
|
||||
resetForm()
|
||||
itemid.value = ''
|
||||
if (data) {
|
||||
isEdit.value = true
|
||||
form.type = 'permission'
|
||||
form.system_id = data.meta.systemId
|
||||
form.parent_id = data.id
|
||||
form.name = row.title
|
||||
form.code = row.authMark
|
||||
itemid.value = row.id
|
||||
} else if (padd) {
|
||||
nextTick(() => {
|
||||
form.parent_id = row.id
|
||||
form.system_id = row.meta.systemId
|
||||
})
|
||||
} else if (row && !padd) {
|
||||
isEdit.value = true
|
||||
nextTick(() => {
|
||||
// 回显数据
|
||||
itemid.value = row.id
|
||||
form.system_id = row.meta.systemId
|
||||
form.parent_id = row.meta.parentId == 0 ? '' : row.meta.parentId
|
||||
form.name = row.name
|
||||
form.type = row.meta.type
|
||||
form.url = row.path
|
||||
form.code = row.meta.code
|
||||
form.icon = row.meta.icon
|
||||
form.sort = row.meta.sort
|
||||
form.is_home = row.meta.isHome
|
||||
form.keep_alive = row.meta.keepAlive
|
||||
form.is_hide = row.meta.isHide
|
||||
form.is_hide_tab = row.meta.isHideTab
|
||||
form.no_auth = row.meta.noAuth
|
||||
form.status = row.meta.status
|
||||
form.link = row.meta.link
|
||||
form.open_type = row.meta.openType
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
Object.assign(form, {
|
||||
// 菜单
|
||||
system_id: '',
|
||||
type: 'menu', // 资源类型
|
||||
parent_id: '',
|
||||
name: '',
|
||||
url: '',
|
||||
code: '',
|
||||
icon: '',
|
||||
status: 1,
|
||||
sort: 1,
|
||||
is_home: 0,
|
||||
keep_alive: 1,
|
||||
is_hide: 0,
|
||||
is_hide_tab: 0,
|
||||
no_auth: 0,
|
||||
link: '',
|
||||
open_type: 'internal'
|
||||
})
|
||||
}
|
||||
|
||||
const deleteMenu = async (item: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该菜单吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const res = await systemService.menuDelete({ id: item.id })
|
||||
ElMessage.success(res?.message || '删除成功!')
|
||||
// 刷新表格数据
|
||||
getTableData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAuth = async (item: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该权限吗?删除后无法恢复', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
const res = await systemService.menuDelete({ id: item.id })
|
||||
ElMessage.success(res?.message || '删除成功!')
|
||||
// 刷新表格数据
|
||||
getTableData()
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
// 添加一个控制变量
|
||||
// const lockMenuType = ref(false)
|
||||
|
||||
const isExpanded = ref(false)
|
||||
const tableRef = ref()
|
||||
|
||||
// 数据请求并发保护计数器
|
||||
let requestCounter = 0
|
||||
|
||||
const toggleExpand = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value) {
|
||||
tableRef.value[isExpanded.value ? 'expandAll' : 'collapseAll']()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-page {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
|
||||
:deep(.small-btn) {
|
||||
height: 22px !important;
|
||||
padding: 0 5px !important;
|
||||
font-size: 10px !important;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
:deep(.el-dialog__footer) {
|
||||
z-index: inherit;
|
||||
.el-button > span {
|
||||
z-index: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
.btnadds {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 10px;
|
||||
color: var(--art-gray-700);
|
||||
cursor: pointer;
|
||||
background-color: rgba(var(--art-gray-200-rgb), 0.8);
|
||||
border-radius: 6px;
|
||||
transition: color 0.3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<div class="customer-page">
|
||||
<div class="customer-toolbar boxshadow">
|
||||
<div class="customer-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input style="min-width: 120px" v-model="keyword" placeholder="请输入关键字" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<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('system@notice_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="msg_no" width="240px" />
|
||||
<ElTableColumn label="通知标题" prop="title" />
|
||||
<el-table-column prop="id" label="通知内容">
|
||||
<template #default="scope">
|
||||
<span v-html="scope.row.content" style="white-space: pre-line"></span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<ElTableColumn label="推送类型" prop="push_method_name" />
|
||||
<ElTableColumn label="场景" prop="scene_name" />
|
||||
<ElTableColumn label="创建时间" prop="created_at" />
|
||||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showfn(scope.row)"> 详情 </el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteRole(scope.row)"
|
||||
v-if="hasAuth('system@notice_destroy')"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
<ElDialog v-model="dialogVisible" title="新增通知" 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="content">
|
||||
<ElInput v-model="form.content" type="textarea" :rows="3" placeholder="请输入通知内容" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="通知用户" prop="recipient">
|
||||
<el-select
|
||||
v-model="form.recipient"
|
||||
multiple
|
||||
filterable
|
||||
placeholder="请选择要通知的用户"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in companyUserList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-text class="mx-1" type="warning">不选择用户,代表通知所有用户!</el-text>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
<ElDialog v-model="dialogVisiblexq" title="通知详情" align-center width="640px">
|
||||
<div class="detail-dialog">
|
||||
<div class="section">
|
||||
<div class="info-grid">
|
||||
<div class="label">发送人</div>
|
||||
<div class="value">{{ noticeDetail.sender_name || '-' }}</div>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="label">通知标题</div>
|
||||
<div class="value">{{ noticeDetail.title || '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="label" style="margin-bottom: 8px">通知内容</div>
|
||||
<div class="content-box">
|
||||
<div class="message-text" v-html="noticeDetail.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { systemService } from '@/api/systemApi'
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'AdminNotice' })
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogVisiblexq = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const { width } = useWindowSize()
|
||||
const keyword = ref<string>('')
|
||||
const isMobile = computed(() => width.value < 500)
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
|
||||
abbr: [{ 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: '',
|
||||
content: '',
|
||||
recipient: []
|
||||
})
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
getWorkList()
|
||||
})
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
getWorkList()
|
||||
})
|
||||
const noticeDetail = ref<any>({})
|
||||
const showfn = async (row: any) => {
|
||||
dialogVisiblexq.value = true
|
||||
await systemService.getNoticeDetail({ id: row.id }).then((res: any) => {
|
||||
noticeDetail.value = res
|
||||
console.log(res)
|
||||
})
|
||||
}
|
||||
// 处理表格分页变化
|
||||
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 systemService.getNoticeList({
|
||||
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) => {
|
||||
getCompanyUserList()
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
form.title = ''
|
||||
form.content = ''
|
||||
form.recipient = []
|
||||
}
|
||||
const deleteRole = (row?: any) => {
|
||||
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await systemService.delNotice({
|
||||
id: row.id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
getWorkList()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
const { message } = await systemService.sendNotice(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;
|
||||
}
|
||||
.customer-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.page-content {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
.customer-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.customer-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.message-text {
|
||||
padding: 10px 14px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
}
|
||||
.detail-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.section {
|
||||
background: var(--el-fill-color-blank);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 8px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 96px 1fr;
|
||||
gap: 6px 12px;
|
||||
align-items: center;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.label {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.value {
|
||||
color: var(--el-text-color-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
.content-box {
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,561 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ElRow>
|
||||
<ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput placeholder="角色名称"></ElInput>
|
||||
</ElCol>
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<ElButton v-ripple>搜索</ElButton>
|
||||
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@role_add')"
|
||||
>新增角色</ElButton
|
||||
>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="roleList"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="角色名称" prop="name" />
|
||||
<ElTableColumn label="描述" prop="desc" />
|
||||
<ElTableColumn label="启用" prop="enable">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status == 1 ? 'primary' : 'info'">
|
||||
{{ scope.row.status == 1 ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at"></ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showPermissionDialog(scope.row)">
|
||||
<!-- <el-button
|
||||
link
|
||||
@click="showPermissionDialog(scope.row)"
|
||||
v-if="hasAuth('system@role_perm')"
|
||||
> -->
|
||||
菜单权限
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('system@role_update')"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteRole(scope.row)"
|
||||
v-if="hasAuth('system@role_destroy')"
|
||||
>删除
|
||||
</el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
|
||||
width="30%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<ElFormItem label="角色名称" prop="name">
|
||||
<ElInput v-model="form.name" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="描述" prop="desc">
|
||||
<ElInput v-model="form.desc" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInput type="number" 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>
|
||||
|
||||
<ElDialog v-model="permissionDialog" title="菜单权限" align-center>
|
||||
<el-tabs v-model="activeName" class="demo-tabs">
|
||||
<el-tab-pane
|
||||
v-for="sys in systemsList"
|
||||
:key="sys.id"
|
||||
:label="sys.name"
|
||||
:name="String(sys.id)"
|
||||
>
|
||||
<ElScrollbar height="70vh">
|
||||
<ElTree
|
||||
:ref="(el:any) => setTreeRef(sys.id, el)"
|
||||
:data="processedMenuBySystem[sys.id] || []"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-expand-all="isExpandAll"
|
||||
:props="defaultProps"
|
||||
@check="() => handleTreeCheck(sys.id)"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span v-if="data.isAuth">
|
||||
{{ data.label }}
|
||||
</span>
|
||||
<span v-else>{{ defaultProps.label(data) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</ElScrollbar>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton type="primary" @click="savePermission">保存</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { formatMenuTitle } from '@/router/utils/utils'
|
||||
import { systemService } from '@/api/systemApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
const { hasAuth } = useAuth()
|
||||
// import { ButtonMoreItem } from '@/components/core/forms/ArtButtonMore.vue'
|
||||
// import { Role, ROLE_LIST_DATA } from '@/mock/temp/formData'
|
||||
const loading = ref(false)
|
||||
defineOptions({ name: 'Role' })
|
||||
const activeName = ref('1')
|
||||
const dialogVisible = ref(false)
|
||||
const permissionDialog = ref(false)
|
||||
// 权限树数据(来自接口 getPermissionMenuList)
|
||||
const permissionMenuList = ref<any[]>([])
|
||||
const permissionMenuListwork = ref<any[]>([])
|
||||
const permissionMenuListcrm = ref<any[]>([])
|
||||
// 精简后的多系统映射
|
||||
const permissionMenuBySystem = reactive<Record<number, any[]>>({})
|
||||
const processedMenuBySystem = reactive<Record<number, any[]>>({})
|
||||
const treeRefs = reactive<Record<number, any>>({})
|
||||
const setTreeRef = (systemId: number, el: any) => {
|
||||
if (el) treeRefs[systemId] = el
|
||||
}
|
||||
const treeRef = ref()
|
||||
const treeRefwork = ref()
|
||||
const treeRefcrm = ref()
|
||||
const isExpandAll = ref(true)
|
||||
const isSelectAll = ref(false)
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
des: [{ required: true, message: '请输入角色描述', trigger: 'blur' }]
|
||||
})
|
||||
//系统
|
||||
const systemsList = ref<any>()
|
||||
const getsystemsList = async () => {
|
||||
const response = await UserService.getsystemsList()
|
||||
systemsList.value = response || []
|
||||
}
|
||||
const form = reactive<any>({
|
||||
id: '',
|
||||
name: '',
|
||||
desc: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
const roleList = ref<any[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getTableData()
|
||||
})
|
||||
|
||||
// const getTableData = () => {
|
||||
// roleList.value = ROLE_LIST_DATA
|
||||
// }
|
||||
// 获取列表数据
|
||||
const getTableData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
|
||||
const { records, current, size, total } = await systemService.getRoleList({
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
})
|
||||
roleList.value = records.map((item: any) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getTableData()
|
||||
}
|
||||
const dialogType = ref('add')
|
||||
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
form.id = row.id
|
||||
form.name = row.name
|
||||
form.desc = row.desc
|
||||
form.sort = row.sort
|
||||
form.status = row.status
|
||||
} else {
|
||||
form.name = ''
|
||||
form.desc = ''
|
||||
form.sort = 0
|
||||
form.status = 1
|
||||
}
|
||||
}
|
||||
const roleId = ref('')
|
||||
const showPermissionDialog = async (row: any) => {
|
||||
permissionDialog.value = true
|
||||
roleId.value = row.id
|
||||
try {
|
||||
await getsystemsList()
|
||||
// 并行请求所选用户的各系统权限
|
||||
const sysIds = (systemsList.value || []).map((s: any) => s.id)
|
||||
const results = await Promise.all(
|
||||
sysIds.map((id: number) =>
|
||||
systemService.getPermissionMenuList({ role_id: row.id, system_id: id })
|
||||
)
|
||||
)
|
||||
sysIds.forEach((id: number, idx: number) => {
|
||||
const raw = results[idx] as any
|
||||
const list = raw?.data ?? raw?.records ?? raw
|
||||
permissionMenuBySystem[id] = Array.isArray(list) ? list : []
|
||||
processedMenuBySystem[id] = buildProcessedMenu(permissionMenuBySystem[id])
|
||||
})
|
||||
// 兼容旧变量,避免其它逻辑引用报错
|
||||
permissionMenuList.value = permissionMenuBySystem[1] || []
|
||||
permissionMenuListwork.value = permissionMenuBySystem[2] || []
|
||||
permissionMenuListcrm.value = permissionMenuBySystem[3] || []
|
||||
// 设置默认选中
|
||||
nextTick(() => {
|
||||
sysIds.forEach((id: number) => {
|
||||
const keys = buildDefaultCheckedKeys(permissionMenuBySystem[id] || [])
|
||||
treeRefs[id]?.setCheckedKeys?.(keys)
|
||||
handleTreeCheck(id)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
permissionMenuList.value = []
|
||||
permissionMenuListwork.value = []
|
||||
permissionMenuListcrm.value = []
|
||||
}
|
||||
}
|
||||
// 构建默认选中的 key 列表(与 node-key="id" 一致)
|
||||
const buildDefaultCheckedKeys = (source: any[]): Array<string | number> => {
|
||||
const keys: Array<string | number> = []
|
||||
const walk = (node: any) => {
|
||||
const hasChildren = Array.isArray(node?.children) && node.children.length > 0
|
||||
const hasAuths = Array.isArray(node?.meta?.authList) && node.meta.authList.length > 0
|
||||
// 仅当没有子节点且没有权限子项时,才勾选父节点,避免联动导致子级被全选
|
||||
if (node?.meta?.selected && !hasChildren && !hasAuths) {
|
||||
keys.push(node.id)
|
||||
}
|
||||
if (Array.isArray(node?.meta?.authList)) {
|
||||
node.meta.authList.forEach((auth: any) => {
|
||||
if (auth?.selected) {
|
||||
keys.push(`${node.id}__auth__${auth.authMark}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (hasChildren) node.children.forEach(walk)
|
||||
}
|
||||
source.forEach(walk)
|
||||
return keys
|
||||
}
|
||||
// 处理菜单数据,将 authList 转换为子节点
|
||||
const processedMenuList = computed(() => {
|
||||
const processNode = (node: any) => {
|
||||
// 规范化 id,确保树节点唯一键与 node-key 对应
|
||||
const processed: any = { ...node, id: node.id }
|
||||
// 如果有 authList,将其转换为子节点
|
||||
if (node.meta && node.meta.authList && node.meta.authList.length) {
|
||||
const authNodes = node.meta.authList.map((auth: any) => ({
|
||||
// 为权限子节点提供稳定唯一 id(数字+字符串),与 node-key="id" 匹配
|
||||
id: `${node.id}__auth__${auth.authMark}`,
|
||||
name: `${node.name}_${auth.authMark}`,
|
||||
label: auth.title,
|
||||
auth_mark: auth.authMark,
|
||||
auth_id: auth.id,
|
||||
isAuth: true,
|
||||
checked: auth.selected || false
|
||||
}))
|
||||
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
|
||||
}
|
||||
// 递归处理子节点
|
||||
if (processed.children) {
|
||||
processed.children = processed.children.map(processNode)
|
||||
}
|
||||
return processed
|
||||
}
|
||||
const source = permissionMenuList.value || []
|
||||
return source.map(processNode)
|
||||
})
|
||||
const processedMenuListwork = computed(() => {
|
||||
const processNode = (node: any) => {
|
||||
// 规范化 id,确保树节点唯一键与 node-key 对应
|
||||
const processed: any = { ...node, id: node.id }
|
||||
// 如果有 authList,将其转换为子节点
|
||||
if (node.meta && node.meta.authList && node.meta.authList.length) {
|
||||
const authNodes = node.meta.authList.map((auth: any) => ({
|
||||
// 为权限子节点提供稳定唯一 id(数字+字符串),与 node-key="id" 匹配
|
||||
id: `${node.id}__auth__${auth.authMark}`,
|
||||
name: `${node.name}_${auth.authMark}`,
|
||||
label: auth.title,
|
||||
auth_mark: auth.authMark,
|
||||
auth_id: auth.id,
|
||||
isAuth: true,
|
||||
checked: auth.selected || false
|
||||
}))
|
||||
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
|
||||
}
|
||||
// 递归处理子节点
|
||||
if (processed.children) {
|
||||
processed.children = processed.children.map(processNode)
|
||||
}
|
||||
return processed
|
||||
}
|
||||
const source = permissionMenuListwork.value || []
|
||||
return source.map(processNode)
|
||||
})
|
||||
const processedMenuListcrm = computed(() => {
|
||||
const processNode = (node: any) => {
|
||||
// 规范化 id,确保树节点唯一键与 node-key 对应
|
||||
const processed: any = { ...node, id: node.id }
|
||||
// 如果有 authList,将其转换为子节点
|
||||
if (node.meta && node.meta.authList && node.meta.authList.length) {
|
||||
const authNodes = node.meta.authList.map((auth: any) => ({
|
||||
// 为权限子节点提供稳定唯一 id(数字+字符串),与 node-key="id" 匹配
|
||||
id: `${node.id}__auth__${auth.authMark}`,
|
||||
name: `${node.name}_${auth.authMark}`,
|
||||
label: auth.title,
|
||||
auth_mark: auth.authMark,
|
||||
auth_id: auth.id,
|
||||
isAuth: true,
|
||||
checked: auth.selected || false
|
||||
}))
|
||||
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
|
||||
}
|
||||
// 递归处理子节点
|
||||
if (processed.children) {
|
||||
processed.children = processed.children.map(processNode)
|
||||
}
|
||||
return processed
|
||||
}
|
||||
const source = permissionMenuListcrm.value || []
|
||||
return source.map(processNode)
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
children: 'children',
|
||||
label: (data: any) => formatMenuTitle(data.meta?.title) || ''
|
||||
}
|
||||
|
||||
// 通用:处理节点,展开权限为子节点
|
||||
const processNode = (node: any) => {
|
||||
const processed: any = { ...node, id: node.id }
|
||||
if (node.meta && node.meta.authList && node.meta.authList.length) {
|
||||
const authNodes = node.meta.authList.map((auth: any) => ({
|
||||
id: `${node.id}__auth__${auth.authMark}`,
|
||||
name: `${node.name}_${auth.authMark}`,
|
||||
label: auth.title,
|
||||
auth_mark: auth.authMark,
|
||||
auth_id: auth.id,
|
||||
isAuth: true,
|
||||
checked: auth.selected || false
|
||||
}))
|
||||
processed.children = processed.children ? [...processed.children, ...authNodes] : authNodes
|
||||
}
|
||||
if (processed.children) processed.children = processed.children.map(processNode)
|
||||
return processed
|
||||
}
|
||||
const buildProcessedMenu = (source: any[]) => (source || []).map(processNode)
|
||||
|
||||
const deleteRole = async (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该角色吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const res = await systemService.roleDelete({ id: row.id })
|
||||
ElMessage.success(res?.message || '删除成功!')
|
||||
getTableData()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
if (dialogType.value === 'add') {
|
||||
// 新增角色逻辑
|
||||
const res = await systemService.roleAdd(form)
|
||||
ElMessage.success(res?.message || '提交成功!')
|
||||
getTableData()
|
||||
} else {
|
||||
// 编辑角色逻辑
|
||||
const res = await systemService.roleEdit(form)
|
||||
ElMessage.success(res?.message || '提交成功!')
|
||||
getTableData()
|
||||
}
|
||||
dialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
}
|
||||
const getSelectedIdsBySystem = (systemId: number) => {
|
||||
const tree = treeRefs[systemId]
|
||||
if (!tree) return [] as (string | number)[]
|
||||
const checkedNodes = tree.getCheckedNodes?.(false) || []
|
||||
const halfCheckedNodes = tree.getHalfCheckedNodes?.() || []
|
||||
const allNodes = [...checkedNodes, ...halfCheckedNodes]
|
||||
const menuIds = Array.from(
|
||||
new Set(allNodes.filter((n: any) => !n.isAuth && n.id != null).map((n: any) => n.id))
|
||||
)
|
||||
const authIds = Array.from(
|
||||
new Set(
|
||||
checkedNodes.filter((n: any) => n.isAuth && n.auth_id != null).map((n: any) => n.auth_id)
|
||||
)
|
||||
)
|
||||
return Array.from(new Set([...(menuIds as any[]), ...(authIds as any[])]))
|
||||
}
|
||||
const savePermission = async () => {
|
||||
const sysIds = (systemsList.value || []).map((s: any) => s.id)
|
||||
const idall = Array.from(new Set(sysIds.flatMap((id: number) => getSelectedIdsBySystem(id))))
|
||||
console.log('idall:', idall)
|
||||
const res = await systemService.roleAuth({
|
||||
role_id: roleId.value,
|
||||
resources: idall
|
||||
})
|
||||
ElMessage.success(res?.message || '权限保存成功')
|
||||
permissionDialog.value = false
|
||||
}
|
||||
const toggleExpandAll = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
// 使用store.nodesMap直接控制所有节点的展开状态
|
||||
const nodes = tree.store.nodesMap
|
||||
for (const node in nodes) {
|
||||
nodes[node].expanded = !isExpandAll.value
|
||||
}
|
||||
|
||||
isExpandAll.value = !isExpandAll.value
|
||||
}
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
if (!isSelectAll.value) {
|
||||
// 全选:获取所有节点的key并设置为选中
|
||||
const allKeys = getAllNodeKeys(processedMenuList.value)
|
||||
tree.setCheckedKeys(allKeys)
|
||||
} else {
|
||||
// 取消全选:清空所有选中
|
||||
tree.setCheckedKeys([])
|
||||
}
|
||||
|
||||
isSelectAll.value = !isSelectAll.value
|
||||
}
|
||||
|
||||
const getAllNodeKeys = (nodes: any[]): Array<string | number> => {
|
||||
const keys: Array<string | number> = []
|
||||
const traverse = (nodeList: any[]) => {
|
||||
nodeList.forEach((node) => {
|
||||
if (node.id !== undefined && node.id !== null) {
|
||||
keys.push(node.id)
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
traverse(node.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
traverse(nodes)
|
||||
return keys
|
||||
}
|
||||
|
||||
const handleTreeCheck = (systemId?: number) => {
|
||||
const sysId = systemId ?? Number(activeName.value)
|
||||
const tree = treeRefs[sysId] || treeRef.value
|
||||
if (!tree) return
|
||||
const checkedKeys = tree.getCheckedKeys?.() || []
|
||||
const allKeys = getAllNodeKeys(processedMenuBySystem[sysId] || [])
|
||||
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
|
||||
}
|
||||
const handleTreeCheckwork = () => {
|
||||
const tree = treeRefwork.value
|
||||
if (!tree) return
|
||||
|
||||
// 使用树组件的getCheckedKeys方法获取选中的节点
|
||||
const checkedKeys = tree.getCheckedKeys()
|
||||
const allKeys = getAllNodeKeys(processedMenuListwork.value)
|
||||
|
||||
// 判断是否全选:选中的节点数量等于总节点数量
|
||||
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
|
||||
}
|
||||
const handleTreeCheckcrm = () => {
|
||||
const tree = treeRefcrm.value
|
||||
if (!tree) return
|
||||
|
||||
// 使用树组件的getCheckedKeys方法获取选中的节点
|
||||
const checkedKeys = tree.getCheckedKeys()
|
||||
const allKeys = getAllNodeKeys(processedMenuListcrm.value)
|
||||
|
||||
// 判断是否全选:选中的节点数量等于总节点数量
|
||||
isSelectAll.value = checkedKeys.length === allKeys.length && allKeys.length > 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-content {
|
||||
.svg-icon {
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
overflow: hidden;
|
||||
vertical-align: -8px;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div v-if="!isMobile">
|
||||
<ElRow>
|
||||
<!-- <ElCol :xs="24" :sm="12" :lg="6">
|
||||
<ElInput placeholder="名称"></ElInput>
|
||||
</ElCol> -->
|
||||
<div style="width: 12px"></div>
|
||||
<ElCol :xs="24" :sm="12" :lg="6" class="el-col2">
|
||||
<!-- <ElButton v-ripple>搜索</ElButton> -->
|
||||
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('system@systems_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="ID" prop="id" width="80" />
|
||||
<ElTableColumn label="系统编码" prop="code" />
|
||||
<ElTableColumn label="系统名称" prop="name" />
|
||||
<ElTableColumn label="系统类型" prop="type">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getTypeTagType(scope.row.type)">
|
||||
{{ getTypeText(scope.row.type) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="系统描述" prop="desc" show-overflow-tooltip />
|
||||
<ElTableColumn label="域名URL" prop="domain_url" show-overflow-tooltip />
|
||||
<ElTableColumn label="区分角色" prop="is_role">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.is_role ? 'success' : 'info'">
|
||||
{{ scope.row.is_role ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<!-- <ElTableColumn label="排序" prop="sort" width="80" /> -->
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'primary' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '禁用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" />
|
||||
<ElTableColumn fixed="right" label="操作">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('system@systems_update')"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteRole(scope.row)"
|
||||
v-if="hasAuth('system@systems_destroy')"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增' : '编辑'"
|
||||
width="60%"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="160px">
|
||||
<ElFormItem label="系统编码" prop="code">
|
||||
<ElInput v-model="form.code" placeholder="请输入系统编码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="系统名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="请输入系统名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="系统类型" prop="type">
|
||||
<ElSelect v-model="form.type" placeholder="请选择系统类型" style="width: 100%">
|
||||
<ElOption label="管理后台" value="admin" />
|
||||
<ElOption label="Web端" value="web" />
|
||||
<ElOption label="小程序" value="miniapp" />
|
||||
<ElOption label="工具" value="tool" />
|
||||
<ElOption label="其他" value="other" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="系统描述" prop="desc">
|
||||
<ElInput v-model="form.desc" type="textarea" :rows="3" placeholder="请输入系统描述" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="系统图标" prop="icon">
|
||||
<ElUpload
|
||||
v-model:file-list="iconFileList"
|
||||
:show-file-list="false"
|
||||
:headers="uploadHeaders"
|
||||
:action="actionImageUplode"
|
||||
:before-upload="beforeIconUpload"
|
||||
:on-success="handleIconSuccess"
|
||||
:on-error="handleIconError"
|
||||
accept="image/*"
|
||||
class="icon-uploader"
|
||||
>
|
||||
<div v-if="form.icon" class="icon-preview">
|
||||
<img :src="file_url" alt="系统图标" class="icon-image" />
|
||||
<div class="icon-overlay">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<el-icon v-else class="icon-uploader-icon"><Plus /></el-icon>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="域名URL" prop="domain_url">
|
||||
<ElInput v-model="form.domain_url" placeholder="请输入域名URL" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="form.sort" :min="0" placeholder="请输入排序" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="是否区分角色权限" prop="is_role">
|
||||
<ElSwitch v-model="form.is_role" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态" prop="status">
|
||||
<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 { Plus } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { systemService } from '@/api/systemApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'Systems' })
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const iconFileList = ref<any[]>([])
|
||||
const typeList = ref<any[]>([])
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = computed(() => width.value < 500)
|
||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||
const { uploadHeaders } = useUploadHeaders()
|
||||
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
|
||||
const rules = reactive<FormRules>({
|
||||
code: [{ required: true, message: '请输入系统编码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入系统名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择系统类型', trigger: 'change' }]
|
||||
// desc: [
|
||||
// { required: true, message: '请输入系统描述', trigger: 'blur' }
|
||||
// ],
|
||||
// icon: [
|
||||
// { required: true, message: '请输入系统图标', trigger: 'blur' }
|
||||
// ],
|
||||
// domain_url: [
|
||||
// { required: true, message: '请输入域名URL', trigger: 'blur' }
|
||||
// ],
|
||||
// is_role: [
|
||||
// { required: true, message: '请选择是否区分角色权限', trigger: 'change' }
|
||||
// ],
|
||||
// sort: [
|
||||
// { required: true, message: '请输入排序', trigger: 'blur' }
|
||||
// ],
|
||||
// status: [
|
||||
// { required: true, message: '请选择状态', trigger: 'change' }
|
||||
// ]
|
||||
})
|
||||
const file_url = ref('')
|
||||
const form = reactive({
|
||||
code: '',
|
||||
name: '',
|
||||
type: '',
|
||||
desc: '',
|
||||
icon: '',
|
||||
domain_url: '',
|
||||
is_role: false,
|
||||
sort: 0,
|
||||
status: true,
|
||||
id: ''
|
||||
})
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表格实例引用
|
||||
const tableRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
getWorkList()
|
||||
})
|
||||
|
||||
// 获取列表数据
|
||||
const getWorkList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
|
||||
const { records, current, size, total } = await systemService.getSystemList({
|
||||
current: currentPage,
|
||||
size: pageSize
|
||||
})
|
||||
tableData.value = records.map((item: any, index: number) => ({
|
||||
...item
|
||||
}))
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取列表失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
// 处理表格分页变化
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getWorkList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getWorkList()
|
||||
}
|
||||
// 系统类型相关辅助函数
|
||||
const getTypeText = (type: string) => {
|
||||
const typeMap: { [key: string]: string } = {
|
||||
'admin': '管理后台',
|
||||
'web': 'Web端',
|
||||
'miniapp': '小程序',
|
||||
'tool': '工具',
|
||||
'other': '其他'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
}
|
||||
|
||||
const getTypeTagType = (type: string) => {
|
||||
const typeMap: { [key: string]: 'primary' | 'success' | 'warning' | 'info' | 'danger' } = {
|
||||
'admin': 'danger',
|
||||
'web': 'primary',
|
||||
'miniapp': 'success',
|
||||
'tool': 'warning',
|
||||
'other': 'info'
|
||||
}
|
||||
return typeMap[type] || 'info'
|
||||
}
|
||||
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type as 'add' | 'edit'
|
||||
|
||||
if (type === 'edit' && row) {
|
||||
file_url.value = row.icon
|
||||
// 编辑模式,填充所有字段
|
||||
Object.assign(form, {
|
||||
code: row.code || '',
|
||||
name: row.name || '',
|
||||
type: row.type || 'web',
|
||||
desc: row.desc || '',
|
||||
icon: row.icon || row.icon_url || '', // 优先使用 icon,其次 icon_url
|
||||
domain_url: row.domain_url || '',
|
||||
is_role: row.is_role !== undefined ? row.is_role : 0,
|
||||
sort: row.sort || 0,
|
||||
status: row.status !== undefined ? row.status : 1,
|
||||
id: row.id || '' // 编辑时填充文件名,如果没有则使用 icon
|
||||
})
|
||||
} else {
|
||||
file_url.value = ''
|
||||
// 新增模式,重置所有字段
|
||||
Object.assign(form, {
|
||||
code: '',
|
||||
name: '',
|
||||
type: 'web',
|
||||
desc: '',
|
||||
icon: '',
|
||||
domain_url: '',
|
||||
is_role: 0,
|
||||
sort: 0,
|
||||
status: 1,
|
||||
id: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
const deleteRole = (row?: any) => {
|
||||
ElMessageBox.confirm('确定删除吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await systemService.systemDelete({
|
||||
id: row.id,
|
||||
})
|
||||
ElMessage.success(message)
|
||||
getWorkList()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (dialogType.value === 'add') {
|
||||
const { message } = await systemService.systemAdd({
|
||||
code: form.code,
|
||||
name: form.name,
|
||||
type: form.type,
|
||||
desc: form.desc,
|
||||
icon: form.icon,
|
||||
domain_url: form.domain_url,
|
||||
is_role: form.is_role,
|
||||
sort: form.sort,
|
||||
status: form.status
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await systemService.systemEdit({
|
||||
id: form.id,
|
||||
code: form.code,
|
||||
name: form.name,
|
||||
type: form.type,
|
||||
desc: form.desc,
|
||||
icon: form.icon,
|
||||
domain_url: form.domain_url,
|
||||
is_role: form.is_role,
|
||||
sort: form.sort,
|
||||
status: form.status
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getWorkList()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 图标上传相关函数
|
||||
const beforeIconUpload = (file: File) => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < 2
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const handleIconSuccess = (response: any, file: any) => {
|
||||
// 接口返回 file_url 用于回显,file_name 用于传参
|
||||
if (response.data) {
|
||||
form.icon = response.data.file_url || response.data.file_name // 回显用 file_url
|
||||
file_url.value = response.data.file_url
|
||||
} else {
|
||||
form.icon = response.file_url || response.file_name
|
||||
file_url.value = response.file_url
|
||||
}
|
||||
ElMessage.success('图标上传成功')
|
||||
}
|
||||
|
||||
const handleIconError = () => {
|
||||
ElMessage.error('图标上传失败')
|
||||
}
|
||||
</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;
|
||||
}
|
||||
}
|
||||
.el-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.icon-uploader {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
text-align: center;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.icon-uploader-icon:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.icon-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.icon-overlay .el-icon {
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.icon-preview:hover .icon-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||