Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a171793e9c | |||
| 129ee07273 | |||
| 90df51097f | |||
| 7b1fdf3883 | |||
| 1c81d3108c | |||
| 2b877e9b4d | |||
| bee80af5af | |||
| 045ce9289c | |||
| c5ec4bb5e1 | |||
| 9e260ddcd8 | |||
| 6feb3ace75 | |||
| ec257a246d | |||
| fa764852ab | |||
| 65f7f4a123 | |||
| 922cfec81d | |||
| 5c8df36f12 | |||
| 5503a14408 | |||
| 3439b45d9a | |||
| 045d13eb7b | |||
| da690dfa06 | |||
| 853e9591b3 | |||
| 9052bd1027 | |||
| 079d9babb8 | |||
| 91d810b33a | |||
| ebd7479573 | |||
| 459d306504 | |||
| 64e53b6ee9 | |||
| 9bd7c89cb4 | |||
| 8a0d4fc516 | |||
| 58f7d56f40 | |||
| a91e1d7820 | |||
| a9f8c5c4dc | |||
| aefd43f79d | |||
| 1bd9d163ec | |||
| be6093e57d | |||
| d83246ba8c | |||
| 329acaf68d | |||
| b319944007 | |||
| 70d26f0829 | |||
| 0e84bedb67 | |||
| f5deb7b8f7 | |||
| e797cb24de | |||
| 619311dfbe | |||
| b402e522bf | |||
| f950f665a9 | |||
| 8deecc8197 | |||
| 3e24d1905f | |||
| 7542d80e38 | |||
| e5548f95e4 | |||
| d30ea5b7bf | |||
| 0e4160c5ca | |||
| bd23288acd | |||
| 6bfb940881 | |||
| b50f650565 | |||
| ea1dbb7af4 | |||
| 1bfbb95bd9 | |||
| a9f5f76b35 | |||
| 3ea9d3d5df | |||
| 0cef82279f | |||
| 695ebbcc71 | |||
| ffc194a1c1 | |||
| 48b95e252f | |||
| 382011d592 | |||
| 0f790269db | |||
| f18cfdb69e | |||
| 858147fbbd | |||
| c333b907e6 | |||
| 7979a31e36 | |||
| 4a39387865 | |||
| fc4de02582 | |||
| 2ffe0c9753 | |||
| c3fb2c48d3 | |||
| c5b0a8265a | |||
| ca06c9bc46 | |||
| 3b6de4c7fa | |||
| 85e8b9b6b4 | |||
| d3bb4ec6e5 | |||
| d7ab6f1a11 | |||
| c7d890809c | |||
| 31cd18fade | |||
| 2c9695fcd1 | |||
| 59c9c97c08 | |||
| c129009b23 |
+5
-3
@@ -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:25703
|
||||
# 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://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
|
||||
+3
-1
@@ -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,4 +3,9 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
*.env
|
||||
.cursorrules
|
||||
src/types/components.d.ts
|
||||
src/types/auto-imports.d.ts
|
||||
.vscode
|
||||
.auto-import.json
|
||||
@@ -1 +0,0 @@
|
||||
pnpm dlx commitlint --edit $1
|
||||
@@ -1 +0,0 @@
|
||||
pnpm run lint:lint-staged
|
||||
Generated
+34
@@ -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",
|
||||
|
||||
+205
-46
@@ -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,81 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
// 绑定付款记录到收款记录
|
||||
static bindPaymentRecord(params:any) {
|
||||
return request.post<Api.Common.AddResponse>({
|
||||
url: '/admin/receipt/payment',
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
+147
-15
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
+6
-31
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
+36
-2
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -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.": "圣文森特和格林纳丁斯"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 313 B |
Binary file not shown.
|
After Width: | Height: | Size: 304 B |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 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,346 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="付款记录详情"
|
||||
width="800px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="payment-detail-dialog" v-loading="loading">
|
||||
<div v-if="paymentData" class="detail-content">
|
||||
<!-- 基本信息 -->
|
||||
<div class="section">
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">基本信息</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>创建日期:</label>
|
||||
<span>{{ formatDate(paymentData.created_at) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>付款来源:</label>
|
||||
<span>{{ paymentData.country_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>汇款人:</label>
|
||||
<span>{{ paymentData.remitter || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>账户:</label>
|
||||
<span>{{ paymentData.account_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>币种:</label>
|
||||
<span>{{ paymentData.currency_name || '—' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>金额:</label>
|
||||
<span class="amount">{{ formatAmount(paymentData.amount, paymentData.currency_name) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 关联信息 -->
|
||||
<div class="section" v-if="paymentData.payment_apply_id">
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">关联信息</span>
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>付款申请ID:</label>
|
||||
<span>{{ paymentData.payment_apply_id }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>关联状态:</label>
|
||||
<el-tag :type="getStatusType(paymentData.status)">
|
||||
{{ getStatusText(paymentData.status) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 收款记录关联 -->
|
||||
<div class="section" v-if="paymentData.receipt_records && paymentData.receipt_records.length">
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">关联收款记录</span>
|
||||
</div>
|
||||
<el-table :data="paymentData.receipt_records" style="margin-top: 12px">
|
||||
<el-table-column label="收款记录ID" prop="id" min-width="120" />
|
||||
<el-table-column label="创建日期" prop="created_at" min-width="140">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" prop="amount" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.amount, scope.row.currency_name) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 备注信息 -->
|
||||
<div class="section" v-if="paymentData.remark">
|
||||
<div class="section-title">
|
||||
<span class="border"></span>
|
||||
<span class="title">备注信息</span>
|
||||
</div>
|
||||
<div class="remark-content">
|
||||
{{ paymentData.remark }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 无数据状态 -->
|
||||
<div v-else-if="!loading" class="no-data">
|
||||
<i class="iconfont-sys"></i>
|
||||
<span>暂无数据</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { financeService } from '@/api/admin/finance'
|
||||
|
||||
interface PaymentRecord {
|
||||
id: string | number
|
||||
created_at: string
|
||||
country_name: string
|
||||
remitter: string
|
||||
account_name: string
|
||||
currency_name: string
|
||||
amount: number
|
||||
status: string
|
||||
payment_apply_id?: string | number
|
||||
receipt_records?: any[]
|
||||
remark?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
paymentId?: string | number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const paymentData = ref<PaymentRecord | null>(null)
|
||||
|
||||
// 获取付款记录详情
|
||||
const getPaymentDetail = async () => {
|
||||
if (!props.paymentId) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await financeService.getPaymentDetail({ id: props.paymentId })
|
||||
paymentData.value = res
|
||||
} catch (error) {
|
||||
console.error('获取付款记录详情失败:', error)
|
||||
paymentData.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
paymentData.value = null
|
||||
}
|
||||
|
||||
// 打印功能
|
||||
const handlePrint = () => {
|
||||
window.print()
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (!dateStr) return '—'
|
||||
try {
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
} catch {
|
||||
return dateStr
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount: number, currency: string) => {
|
||||
return `${currency || ''} ${amount?.toLocaleString() || 0}`
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'available': 'success',
|
||||
'used': 'info',
|
||||
'pending': 'warning',
|
||||
'cancelled': 'danger',
|
||||
'bound': 'success',
|
||||
'unbound': 'info'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'available': '可用',
|
||||
'used': '已使用',
|
||||
'pending': '待处理',
|
||||
'cancelled': '已取消',
|
||||
'bound': '已绑定',
|
||||
'unbound': '未绑定'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal && props.paymentId) {
|
||||
getPaymentDetail()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听付款ID变化
|
||||
watch(() => props.paymentId, (newId) => {
|
||||
if (visible.value && newId) {
|
||||
getPaymentDetail()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-detail-dialog {
|
||||
.detail-content {
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
|
||||
.border {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: #3d7bff;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
min-width: 100px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
|
||||
&.amount {
|
||||
font-weight: 600;
|
||||
color: #3d7bff;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remark-content {
|
||||
padding: 12px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
min-height: 60px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 0;
|
||||
color: #999;
|
||||
|
||||
i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
// 打印样式
|
||||
@media print {
|
||||
.dialog-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payment-detail-dialog {
|
||||
.section-title {
|
||||
.border {
|
||||
background: #000 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="选择付款记录"
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="payment-record-dialog">
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="请输入汇款人、账户或金额"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@keyup.enter="getPaymentRecords"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getPaymentRecords"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="getPaymentRecords">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 付款记录表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="paymentRecords"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
highlight-current-row
|
||||
style="margin-top: 16px"
|
||||
>
|
||||
<el-table-column type="radio" width="55">
|
||||
<template #default="scope">
|
||||
<el-radio
|
||||
v-model="selectedRecordId"
|
||||
:value="scope.row.id"
|
||||
@change="handleRadioChange(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建日期" prop="created_at" min-width="160" />
|
||||
<el-table-column label="付款来源" prop="country_name" min-width="120" />
|
||||
<el-table-column label="汇款人" prop="remitter" min-width="120" />
|
||||
<el-table-column label="账户" prop="account_name" min-width="120" />
|
||||
<el-table-column label="账币" prop="currency_name" min-width="120" />
|
||||
<el-table-column label="金额" prop="amount" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ formatAmount(scope.row.amount, scope.row.currency_name) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.currentPage"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :disabled="!selectedRecord" @click="handleConfirm">
|
||||
确认绑定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { financeService } from '@/api/admin/finance'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface PaymentRecord {
|
||||
id: string | number
|
||||
created_at: string
|
||||
country_name: string
|
||||
remitter: string
|
||||
account_name: string
|
||||
currency_name: string
|
||||
amount: number
|
||||
status: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
receiptId?: string | number
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'confirm', record: PaymentRecord): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const paymentRecords = ref<PaymentRecord[]>([])
|
||||
const selectedRecord = ref<PaymentRecord | null>(null)
|
||||
const selectedRecordId = ref<string | number | null>(null)
|
||||
const tableRef = ref()
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 获取付款记录列表
|
||||
const getPaymentRecords = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
current: pagination.currentPage,
|
||||
size: pagination.pageSize,
|
||||
keyword: searchKeyword.value,
|
||||
scene:'correlation',
|
||||
correlation: 'unbind' // 只显示可用的付款记录
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await financeService.getPaymentList(params)
|
||||
paymentRecords.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取付款记录失败:', error)
|
||||
ElMessage.error('获取付款记录失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理选择变化(保留兼容性)
|
||||
const handleSelectionChange = (selection: PaymentRecord[]) => {
|
||||
// 单选模式下不使用此方法
|
||||
}
|
||||
|
||||
// 处理单选按钮变化
|
||||
const handleRadioChange = (record: PaymentRecord) => {
|
||||
selectedRecord.value = record
|
||||
selectedRecordId.value = record.id
|
||||
}
|
||||
|
||||
// 处理行点击
|
||||
const handleRowClick = (record: PaymentRecord) => {
|
||||
selectedRecord.value = record
|
||||
selectedRecordId.value = record.id
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.pageSize = size
|
||||
pagination.currentPage = 1
|
||||
getPaymentRecords()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.currentPage = page
|
||||
getPaymentRecords()
|
||||
}
|
||||
|
||||
// 确认绑定
|
||||
const handleConfirm = () => {
|
||||
if (!selectedRecord.value) {
|
||||
ElMessage.warning('请选择一条付款记录')
|
||||
return
|
||||
}
|
||||
emit('confirm', selectedRecord.value)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
selectedRecord.value = null
|
||||
selectedRecordId.value = null
|
||||
searchKeyword.value = ''
|
||||
pagination.currentPage = 1
|
||||
}
|
||||
|
||||
// 格式化金额显示
|
||||
const formatAmount = (amount: number, currency: string) => {
|
||||
return `${currency} ${amount?.toLocaleString() || 0}`
|
||||
}
|
||||
|
||||
// 获取状态类型
|
||||
const getStatusType = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'available': 'success',
|
||||
'used': 'info',
|
||||
'pending': 'warning',
|
||||
'cancelled': 'danger'
|
||||
}
|
||||
return statusMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'available': '可用',
|
||||
'used': '已使用',
|
||||
'pending': '待处理',
|
||||
'cancelled': '已取消'
|
||||
}
|
||||
return statusMap[status] || status
|
||||
}
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(visible, (newVal) => {
|
||||
if (newVal) {
|
||||
getPaymentRecords()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.payment-record-dialog {
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
</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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 }
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
)
|
||||
+59
-16
@@ -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
|
||||
|
||||
Vendored
+19
-1
@@ -74,9 +74,15 @@ declare module 'vue' {
|
||||
ContainerSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ContainerSettings.vue')['default']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
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']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
@@ -86,24 +92,36 @@ declare module 'vue' {
|
||||
ElForm: typeof import('element-plus/es')['ElForm']
|
||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
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']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
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']
|
||||
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']
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
|
||||
authList?: Array<{
|
||||
title: string
|
||||
auth_mark: string
|
||||
authMark?: string // 兼容旧版
|
||||
}>
|
||||
/** 是否为一级菜单 */
|
||||
isFirstLevel?: boolean
|
||||
|
||||
Vendored
+32
-2
@@ -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
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,895 @@
|
||||
<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)
|
||||
}
|
||||
// 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.currentPage}&size=${params.pageSize}&user_id=${params.user_id}&tag_id=${params.tag_id}`
|
||||
// 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,235 @@
|
||||
<template>
|
||||
<div class="stocking-page">
|
||||
<div class="stocking-toolbar boxshadow">
|
||||
<div class="toolbar-left"></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">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="未认领" name="not"></el-tab-pane>
|
||||
<el-tab-pane label="已认领" name="already"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<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="created_at" min-width="160" />
|
||||
<ElTableColumn label="付款来源" prop="country_name" min-width="120" />
|
||||
<ElTableColumn label="汇款人" prop="remitter" min-width="120" />
|
||||
<ElTableColumn label="账户" prop="account_name" min-width="120" />
|
||||
<ElTableColumn label="账币" prop="currency_name" min-width="120" />
|
||||
<ElTableColumn label="金额" prop="amount" 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()"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="!scope.row.payment_apply_id"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleClaim(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 认领
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 付款记录选择对话框 -->
|
||||
<PaymentRecordDialog
|
||||
v-model="paymentDialogVisible"
|
||||
:receipt-id="currentReceiptId"
|
||||
@confirm="handlePaymentRecordConfirm"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { manageService } from '@/api/adminApi'
|
||||
import { financeService } from '@/api/admin/finance'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import PaymentRecordDialog from '@/components/custom/PaymentRecordDialog.vue'
|
||||
defineOptions({ name: 'Adminclaim' })
|
||||
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 activeTab = ref('not')
|
||||
|
||||
// 付款记录对话框相关
|
||||
const paymentDialogVisible = ref(false)
|
||||
const currentReceiptId = ref<string | number>('')
|
||||
|
||||
// 审批弹出框相关
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
|
||||
})
|
||||
watch([activeTab], () => {
|
||||
pagination.currentPage = 1
|
||||
getList()
|
||||
})
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
payment_apply: activeTab.value,
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await financeService.getReceiptList(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 = () => {}
|
||||
|
||||
// 处理认领操作
|
||||
const handleClaim = (row: any) => {
|
||||
currentReceiptId.value = row.id
|
||||
paymentDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理付款记录确认绑定
|
||||
const handlePaymentRecordConfirm = async (paymentRecord: any) => {
|
||||
try {
|
||||
const params = {
|
||||
id: currentReceiptId.value,
|
||||
payment_apply_id: paymentRecord.id
|
||||
}
|
||||
|
||||
await financeService.bindPaymentRecord(params)
|
||||
ElMessage.success('认领成功')
|
||||
getList() // 刷新列表
|
||||
} catch (error) {
|
||||
console.error('认领失败:', error)
|
||||
ElMessage.error('认领失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = () => {}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-tabs__nav-wrap:after){
|
||||
background: #fff;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
// 创建收款记录 - 弹窗样式对齐设计图
|
||||
.receipt-dialog {
|
||||
.receipt-form {
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// justify-content: flex-end;
|
||||
// border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.avatarimg {
|
||||
width: 60px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 200px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
:deep(.receipt-dialog .el-dialog__footer) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</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,918 @@
|
||||
<template>
|
||||
<div class="stocking-page">
|
||||
<div class="stocking-toolbar boxshadow">
|
||||
<div class="toolbar-left">
|
||||
<ElButton
|
||||
type="primary"
|
||||
v-ripple
|
||||
@click="handleCreate"
|
||||
v-if="hasAuth('payment_records@add')"
|
||||
>
|
||||
创建付款记录
|
||||
</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="created_at" min-width="140" />
|
||||
<ElTableColumn label="付款来源" prop="country_name" min-width="160" />
|
||||
<ElTableColumn label="汇款人" prop="remitter" min-width="120" />
|
||||
<ElTableColumn label="账户" prop="account_name" min-width="120" />
|
||||
<ElTableColumn label="币种" prop="currency_name" min-width="120" />
|
||||
<ElTableColumn label="金额" prop="amount" min-width="120" />
|
||||
<ElTableColumn label="操作" fixed="right" min-width="140" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="10" justify="center">
|
||||
<el-link
|
||||
v-if="hasAuth('payment_records@show')"
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasAuth('payment_records@update')"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleEdit(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 修改
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasAuth('payment_records@destroy')"
|
||||
type="danger"
|
||||
underline="never"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 删除
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑付款记录 - 通用弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑付款记录' : '创建付款记录'"
|
||||
width="900px"
|
||||
class="receipt-dialog"
|
||||
>
|
||||
<el-form
|
||||
ref="createFormRef"
|
||||
:model="form"
|
||||
:rules="createRules"
|
||||
label-width="120px"
|
||||
class="receipt-form"
|
||||
>
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="阿里订单号/PI号">
|
||||
<el-select
|
||||
v-model="form.payment_apply_id"
|
||||
placeholder="请选择阿里订单号/PI号"
|
||||
@change="fetchOrders"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in piOptions"
|
||||
:key="item.id"
|
||||
:label="item.pi_no"
|
||||
:value="item.pi_no"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="备货单">
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<ElButton
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!form.payment_apply_id"
|
||||
@click="openOrderPicker"
|
||||
>
|
||||
选择备货单
|
||||
</ElButton>
|
||||
<span v-if="Array.isArray(form.stock_id) && form.stock_id.length">
|
||||
已选 {{ form.stock_id.length }} 条
|
||||
</span>
|
||||
</div>
|
||||
<!-- <div
|
||||
v-if="selectedOrders.length"
|
||||
style="margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px"
|
||||
>
|
||||
<el-tag
|
||||
v-for="it in selectedOrders"
|
||||
:key="it.stock_orders_id"
|
||||
closable
|
||||
@close="removeSelectedOrder(it.stock_orders_id)"
|
||||
>
|
||||
{{ it.order_no }}
|
||||
</el-tag>
|
||||
</div> -->
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="业务员">
|
||||
<el-input type="hidden" v-model="form.sales_user_id" placeholder="请填写业务员" />
|
||||
<el-input v-model="form.sales_user_name" placeholder="请填写业务员" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="付款公司">
|
||||
<el-select v-model="form.corp_id" placeholder="请选择付款公司">
|
||||
<el-option
|
||||
v-for="item in corpOptions"
|
||||
:key="item.item_value"
|
||||
:label="item.item_label"
|
||||
:value="item.item_value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="付款来源">
|
||||
<el-select v-model="form.country_id" placeholder="请选择付款国家">
|
||||
<el-option
|
||||
v-for="item in countryOptions"
|
||||
:key="item.id"
|
||||
:label="item.cn_addrs"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="汇款人">
|
||||
<el-input v-model="form.remitter" placeholder="请填写汇款人" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="账户">
|
||||
<el-select v-model="form.account_id" placeholder="请选择账户">
|
||||
<el-option
|
||||
v-for="item in accountOptions"
|
||||
:key="item.id"
|
||||
:label="item.short_desc"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="币种">
|
||||
<el-select
|
||||
v-model="form.currency_id"
|
||||
placeholder="请选择币种"
|
||||
@change="fetchExchangeRate"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in currencyOptions"
|
||||
:key="item.id"
|
||||
:label="item.currency_name"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="汇率">
|
||||
<el-input v-model="form.exchange_rate" placeholder="请填写汇率" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="总金额">
|
||||
<el-input v-model="form.amount" placeholder="请填写总金额" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="汇款行">
|
||||
<el-input v-model="form.swift" placeholder="请填写SWIFT号" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="客户付款水单">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
:action="actionImageUplode"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="handleUploadSuccess"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
>
|
||||
<img :src="imageUrl1" alt="" class="avatarimg" />
|
||||
<input type="hidden" v-model="form.voucher_file" />
|
||||
</el-upload>
|
||||
<div
|
||||
v-if="Array.isArray(form.voucher_file_list) && form.voucher_file_list.length"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
<div
|
||||
v-for="(u, i) in form.voucher_file_list"
|
||||
:key="i"
|
||||
style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px"
|
||||
>
|
||||
<img
|
||||
v-if="/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test((u || '').split('?')[0])"
|
||||
:src="u"
|
||||
style="
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
:href="u"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 200px;
|
||||
"
|
||||
>{{ (u || '').split('?')[0].split('/').pop() }}</a
|
||||
>
|
||||
<el-button link type="danger" @click="removeVoucher(i)" style="width: 80px"
|
||||
>移除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="入账资料提交">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
:action="actionImageUplode"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="handleUploadSuccess1"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
>
|
||||
<img :src="imageUrl1" alt="" class="avatarimg" />
|
||||
<input type="hidden" v-model="form.submit_files" />
|
||||
</el-upload>
|
||||
<div
|
||||
v-if="Array.isArray(form.submit_files_list) && form.submit_files_list.length"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
<div
|
||||
v-for="(u, i) in form.submit_files_list"
|
||||
:key="i"
|
||||
style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px"
|
||||
>
|
||||
<img
|
||||
v-if="/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test((u || '').split('?')[0])"
|
||||
:src="u"
|
||||
style="
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
:href="u"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 200px;
|
||||
"
|
||||
>{{ (u || '').split('?')[0].split('/').pop() }}</a
|
||||
>
|
||||
<el-button link type="danger" @click="removeSubmit(i)" style="width: 80px"
|
||||
>移除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 选择备货单弹窗 -->
|
||||
<el-dialog v-model="orderPicker.visible" title="选择备货单" width="800px" align-center>
|
||||
<ArtTable :data="orderPicker.data" :loading="orderPicker.loading">
|
||||
<template #default>
|
||||
<ElTableColumn width="56" align="center">
|
||||
<template #header>
|
||||
<el-checkbox
|
||||
:indeterminate="isOrderIndeterminate"
|
||||
:model-value="isAllOrdersSelected"
|
||||
@change="toggleSelectAllOrders"
|
||||
/>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-checkbox
|
||||
:model-value="isOrderSelected(scope.row)"
|
||||
@change="() => toggleOrder(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="备货单号" prop="order_no" min-width="160" />
|
||||
<ElTableColumn label="业务员" prop="salesperson_snapshot" min-width="160" />
|
||||
<ElTableColumn label="创建时间" prop="biz_date" min-width="160" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
<template #footer>
|
||||
<ElButton type="primary" @click="orderPicker.visible = false">关闭</ElButton>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 付款记录详情查看对话框 -->
|
||||
<PaymentRecordDetailDialog
|
||||
v-model="detailDialogVisible"
|
||||
:payment-id="currentPaymentId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import { financeService } from '@/api/admin/finance'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
import PaymentRecordDetailDialog from '@/components/custom/PaymentRecordDetailDialog.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { purchaseService } from '@/api/admin/procurement'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import imageUrl1 from '@/assets/img/crm/upimg.png'
|
||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||
const { uploadHeaders } = useUploadHeaders()
|
||||
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=payment_records`
|
||||
defineOptions({ name: 'AdminReceiptRecords' })
|
||||
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 piOptions = ref<any>()
|
||||
const orderOptions = ref<any>()
|
||||
const loadOptions = async () => {
|
||||
try {
|
||||
const res = await purchaseService.purchaseorderpredata({ scene: 'pi_no' })
|
||||
piOptions.value = res || []
|
||||
} catch {
|
||||
piOptions.value = []
|
||||
}
|
||||
}
|
||||
const fetchOrders = async (pi_no: string) => {
|
||||
try {
|
||||
const res = await purchaseService.purchaseorderpredata({
|
||||
scene: 'order_no',
|
||||
pi_no: pi_no
|
||||
})
|
||||
orderOptions.value = res || []
|
||||
// 切换 PI 后,清空已选备货单与业务员信息
|
||||
form.value.stock_id = []
|
||||
form.value.sales_user_name = ''
|
||||
form.value.sales_user_id = ''
|
||||
} catch {
|
||||
orderOptions.value = []
|
||||
}
|
||||
}
|
||||
//备货单选择器(多选)
|
||||
const orderPicker = reactive({ visible: false, loading: false, data: [] as any[] })
|
||||
// const selectedOrders = computed<any[]>(() => {
|
||||
// const ids = new Set((form.value.stock_id || []).map((v) => String(v)))
|
||||
// const source = Array.isArray(orderOptions.value) ? orderOptions.value : []
|
||||
// return source.filter((it: any) => ids.has(String(it.stock_orders_id)))
|
||||
// })
|
||||
const isAllOrdersSelected = computed<boolean>(() => {
|
||||
const data = Array.isArray(orderPicker.data) ? orderPicker.data : []
|
||||
if (!data.length) return false
|
||||
const ids = new Set((form.value.stock_id || []).map((v) => String(v)))
|
||||
return data.every((it: any) => ids.has(String(it.stock_orders_id)))
|
||||
})
|
||||
const isOrderIndeterminate = computed<boolean>(() => {
|
||||
const data = Array.isArray(orderPicker.data) ? orderPicker.data : []
|
||||
const selected = (form.value.stock_id || []).length
|
||||
return selected > 0 && selected < data.length
|
||||
})
|
||||
function openOrderPicker() {
|
||||
if (!form.value.payment_apply_id) return
|
||||
orderPicker.visible = true
|
||||
// 如果未加载,则用现有 orderOptions;如需远程可在此拉取
|
||||
orderPicker.data = Array.isArray(orderOptions.value) ? orderOptions.value : []
|
||||
}
|
||||
function isOrderSelected(row: any) {
|
||||
return (form.value.stock_id || []).some((id) => String(id) === String(row?.stock_orders_id))
|
||||
}
|
||||
function toggleSelectAllOrders() {
|
||||
const data = Array.isArray(orderPicker.data) ? orderPicker.data : []
|
||||
if (!data.length) return
|
||||
if (isAllOrdersSelected.value) {
|
||||
// 取消全选
|
||||
const dataIds = new Set(data.map((d: any) => String(d.stock_orders_id)))
|
||||
form.value.stock_id = (form.value.stock_id || []).filter((id) => !dataIds.has(String(id)))
|
||||
} else {
|
||||
// 全选
|
||||
const existed = new Set((form.value.stock_id || []).map((v) => String(v)))
|
||||
const merge = [...(form.value.stock_id || [])]
|
||||
data.forEach((d: any) => {
|
||||
const id = d?.stock_orders_id
|
||||
if (id != null && !existed.has(String(id))) merge.push(id)
|
||||
})
|
||||
form.value.stock_id = merge
|
||||
if (!form.value.sales_user_id && data.length) {
|
||||
form.value.sales_user_name = data[0]?.salesperson_snapshot || ''
|
||||
form.value.sales_user_id = data[0]?.salesperson_id || ''
|
||||
}
|
||||
}
|
||||
if (!(form.value.stock_id || []).length) {
|
||||
form.value.sales_user_name = ''
|
||||
form.value.sales_user_id = ''
|
||||
}
|
||||
}
|
||||
function toggleOrder(row: any) {
|
||||
const ids = Array.isArray(form.value.stock_id) ? [...form.value.stock_id] : []
|
||||
const id = row?.stock_orders_id
|
||||
if (id == null) return
|
||||
const idx = ids.findIndex((v) => String(v) === String(id))
|
||||
if (idx >= 0) {
|
||||
ids.splice(idx, 1)
|
||||
} else {
|
||||
ids.push(id)
|
||||
}
|
||||
form.value.stock_id = ids
|
||||
// 同步一次业务员(使用第一个所选的业务员信息)
|
||||
if (!form.value.sales_user_id && ids.length) {
|
||||
form.value.sales_user_name = row?.salesperson_snapshot || ''
|
||||
form.value.sales_user_id = row?.salesperson_id || ''
|
||||
}
|
||||
if (!ids.length) {
|
||||
form.value.sales_user_name = ''
|
||||
form.value.sales_user_id = ''
|
||||
}
|
||||
}
|
||||
function removeSelectedOrder(stock_orders_id: string | number) {
|
||||
const next = (form.value.stock_id || []).filter((v) => String(v) !== String(stock_orders_id))
|
||||
form.value.stock_id = next
|
||||
if (next.length === 0) {
|
||||
form.value.sales_user_name = ''
|
||||
form.value.sales_user_id = ''
|
||||
}
|
||||
}
|
||||
//国家
|
||||
const countryOptions = ref<any[]>([])
|
||||
const fetchCountryOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCountrySelect()
|
||||
res.forEach((item: any) => {
|
||||
item.cn_addrs = item.cn_addr + '(' + item.en_addr + ')'
|
||||
})
|
||||
countryOptions.value = res || []
|
||||
} catch {
|
||||
countryOptions.value = []
|
||||
}
|
||||
}
|
||||
//付款公司
|
||||
const corpOptions = ref<any[]>([])
|
||||
const fetchCorpOptions = async () => {
|
||||
try {
|
||||
const response = await commonService.getDictSelect({ scope: '21' })
|
||||
corpOptions.value = response.payment_corp
|
||||
} catch {
|
||||
corpOptions.value = []
|
||||
}
|
||||
}
|
||||
//账户
|
||||
const accountOptions = ref<any[]>([])
|
||||
const fetchAccountOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getAccountSelect()
|
||||
accountOptions.value = res || []
|
||||
} catch {
|
||||
accountOptions.value = []
|
||||
}
|
||||
}
|
||||
//币种
|
||||
const currencyOptions = ref<any[]>([])
|
||||
const fetchCurrencyOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCurrencySelect()
|
||||
currencyOptions.value = res || []
|
||||
} catch {
|
||||
currencyOptions.value = []
|
||||
}
|
||||
}
|
||||
const fetchExchangeRate = async (currency_id: string) => {
|
||||
try {
|
||||
const list = Array.isArray(currencyOptions.value) ? currencyOptions.value : []
|
||||
const picked = list.find((it: any) => String(it?.id ?? '') === String(currency_id))
|
||||
if (!picked) {
|
||||
form.value.exchange_rate = ''
|
||||
return
|
||||
}
|
||||
form.value.exchange_rate = picked?.exchange_rate || ''
|
||||
} catch {
|
||||
form.value.exchange_rate = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 审批弹出框相关
|
||||
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 financeService.getPaymentList(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 dialogVisible = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentId = ref<string | number | null>(null)
|
||||
|
||||
// 详情对话框相关
|
||||
const detailDialogVisible = ref(false)
|
||||
const currentPaymentId = ref<string | number>('')
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const form = ref({
|
||||
payment_apply_id: '',
|
||||
stock_id: [] as (string | number)[],
|
||||
sales_user_id: '',
|
||||
sales_user_name: '',
|
||||
corp_id: '',
|
||||
country_id: '',
|
||||
remitter: '',
|
||||
account_id: '',
|
||||
currency_id: '',
|
||||
exchange_rate: '',
|
||||
amount: '',
|
||||
swift: '',
|
||||
voucher_file: [] as string[],
|
||||
voucher_file_list: [] as string[],
|
||||
submit_files: [] as object[],
|
||||
submit_files_list: [] as string[]
|
||||
})
|
||||
const createRules: FormRules = {
|
||||
payment_apply_id: [{ required: true, message: '请选择阿里订单号/PI号', trigger: 'blur' }],
|
||||
sales_user_id: [{ required: true, message: '请选择业务员', trigger: 'blur' }],
|
||||
corp_id: [{ required: true, message: '请选择付款公司', trigger: 'blur' }],
|
||||
country_id: [{ required: true, message: '请选择付款国家', trigger: 'blur' }],
|
||||
remitter: [{ required: true, message: '请输入汇款人', trigger: 'blur' }],
|
||||
account_id: [{ required: true, message: '请选择账户', trigger: 'blur' }],
|
||||
currency_id: [{ required: true, message: '请选择币种', trigger: 'blur' }],
|
||||
exchange_rate: [{ required: true, message: '请输入汇率', trigger: 'blur' }],
|
||||
amount: [{ required: true, message: '请输入金额', trigger: 'blur' }],
|
||||
swift: [{ required: true, message: '请输入汇款行', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const resetCreateForm = () => {
|
||||
Object.assign(form.value, {
|
||||
payment_apply_id: '',
|
||||
stock_id: [],
|
||||
sales_user_id: '',
|
||||
sales_user_name: '',
|
||||
corp_id: '',
|
||||
country_id: '',
|
||||
remitter: '',
|
||||
account_id: '',
|
||||
currency_id: '',
|
||||
exchange_rate: '',
|
||||
amount: '',
|
||||
swift: '',
|
||||
voucher_file: [],
|
||||
voucher_file_list: [],
|
||||
submit_files: [],
|
||||
submit_files_list: []
|
||||
})
|
||||
currentId.value = null
|
||||
createFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
resetCreateForm()
|
||||
isEdit.value = false
|
||||
loadOptions()
|
||||
fetchAccountOptions()
|
||||
fetchCurrencyOptions()
|
||||
fetchCountryOptions()
|
||||
fetchCorpOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
createFormRef.value?.validate?.(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
createLoading.value = true
|
||||
// 根据新增/编辑分支处理
|
||||
const payload: any = { ...form.value }
|
||||
delete payload.submit_files_list
|
||||
delete payload.voucher_file_list
|
||||
delete payload.sales_user_name
|
||||
payload.voucher_file = payload.voucher_file.join(',')
|
||||
// 将所选备货单 ID 列表传给后端字段(兼容旧字段)
|
||||
if (Array.isArray(payload.stock_id)) {
|
||||
// 若后端仍接收单个 stock_id,可取第一个;否则保留 stock_id
|
||||
payload.stock_id = payload.stock_id ?? payload.stock_id[0]
|
||||
}
|
||||
payload.stock = [
|
||||
// 库存数组
|
||||
{
|
||||
stock_id: payload.stock_id, // 备货单ID
|
||||
pi_on: payload.payment_apply_id
|
||||
}
|
||||
]
|
||||
delete payload.payment_apply_id
|
||||
delete payload.stock_id
|
||||
if (isEdit.value) {
|
||||
const forms = { ...payload, id: currentId.value }
|
||||
const { message } = await financeService.updatePayment(forms)
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
// console.log('payload:', payload)
|
||||
const { message } = await financeService.addPayment(payload)
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getList()
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleView = (row: any) => {
|
||||
currentPaymentId.value = row.id
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
const handleEdit = async (row?: any) => {
|
||||
resetCreateForm()
|
||||
isEdit.value = true
|
||||
currentId.value = row?.id ?? null
|
||||
// 如有需要,这里可按接口字段回填表单
|
||||
const data = await financeService.getPaymentDetail({ id: row.id })
|
||||
row = data || row
|
||||
if (row && typeof row === 'object') {
|
||||
const stockList = Array.isArray(row.stock) ? row.stock : []
|
||||
const firstStock = stockList[0] || {}
|
||||
const piNo = firstStock?.pi_no || row?.payment_apply_id || row?.pi_no || ''
|
||||
form.value.payment_apply_id = piNo || form.value.payment_apply_id
|
||||
// 组装已选备货单ID数组
|
||||
const pickedIds = stockList
|
||||
.map((s: any) => s?.stock_id || s?.stock_orders_id || s?.id)
|
||||
.filter((v: any) => v != null)
|
||||
if (pickedIds.length) {
|
||||
form.value.stock_id = pickedIds
|
||||
} else if (row?.stock_id) {
|
||||
form.value.stock_id = [row.stock_id]
|
||||
}
|
||||
form.value.sales_user_id = row.sales_user_id ?? form.value.sales_user_id
|
||||
form.value.sales_user_name = row.sales_user_name ?? form.value.sales_user_name
|
||||
form.value.corp_id = String(row.corp_id ?? '')
|
||||
form.value.country_id = row.country_id ?? form.value.country_id
|
||||
form.value.remitter = row.remitter ?? form.value.remitter
|
||||
form.value.account_id = row.account_id ?? form.value.account_id
|
||||
form.value.currency_id = row.currency_id ?? form.value.currency_id
|
||||
form.value.exchange_rate = row.exchange_rate ?? form.value.exchange_rate
|
||||
form.value.amount = row.amount ?? form.value.amount
|
||||
form.value.swift = row.swift ?? form.value.swift
|
||||
// 处理客户付款水单文件
|
||||
if (row.voucher_file) {
|
||||
const voucherFiles = Array.isArray(row.voucher_file)
|
||||
? row.voucher_file
|
||||
: String(row.voucher_file)
|
||||
.split(',')
|
||||
.map((s: string) => s.trim())
|
||||
.filter(Boolean)
|
||||
form.value.voucher_file = voucherFiles
|
||||
// 构造预览URL列表(假设文件路径需要拼接域名)
|
||||
const baseUrl = import.meta.env.VITE_API_URL || ''
|
||||
form.value.voucher_file_list = voucherFiles.map((file: string) =>
|
||||
file.startsWith('http') ? file : `${baseUrl}/${file}`
|
||||
)
|
||||
}
|
||||
// 处理入账资料提交文件
|
||||
if (row.submit_files) {
|
||||
const submitFiles = row.submit_files
|
||||
form.value.submit_files = submitFiles
|
||||
// 构造预览URL列表
|
||||
const baseUrl = import.meta.env.VITE_API_URL || ''
|
||||
form.value.submit_files_list = submitFiles.map((file: any) => file.file_name)
|
||||
}
|
||||
}
|
||||
loadOptions()
|
||||
fetchAccountOptions()
|
||||
fetchCurrencyOptions()
|
||||
fetchCountryOptions()
|
||||
fetchCorpOptions()
|
||||
// 拉取当前 PI 的备货单列表用于弹窗展示与勾选回显
|
||||
try {
|
||||
const res = await purchaseService.purchaseorderpredata({
|
||||
scene: 'order_no',
|
||||
pi_no: form.value.payment_apply_id
|
||||
})
|
||||
orderOptions.value = res || []
|
||||
} catch {
|
||||
orderOptions.value = []
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const handleDelete = (row: { id: any }) => {
|
||||
ElMessageBox.confirm('确定删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
financeService.deletePayment({ id: row.id }).then((res) => {
|
||||
ElMessage.success(res.message)
|
||||
getList()
|
||||
})
|
||||
})
|
||||
}
|
||||
function handleUploadSuccess(response: any) {
|
||||
const fileName = response?.data?.file_name || ''
|
||||
const fileUrl = response?.data?.file_url || response?.data?.full_url || response?.url || ''
|
||||
const names = Array.isArray(form.value.voucher_file) ? [...form.value.voucher_file] : []
|
||||
if (fileName) names.push(fileName)
|
||||
form.value.voucher_file = names
|
||||
const list = Array.isArray(form.value.voucher_file_list) ? form.value.voucher_file_list : []
|
||||
list.push(fileUrl)
|
||||
form.value.voucher_file_list = [...list]
|
||||
ElMessage.success(response.message)
|
||||
}
|
||||
function handleUploadSuccess1(response: any) {
|
||||
const fileName = response?.data?.file_name || ''
|
||||
const fileUrl = response?.data?.file_url || response?.data?.full_url || response?.url || ''
|
||||
const names = Array.isArray(form.value.submit_files) ? [...form.value.submit_files] : []
|
||||
const files = ref({
|
||||
file_src: '',
|
||||
file_name: ''
|
||||
})
|
||||
files.value.file_src = fileName
|
||||
files.value.file_name = response?.data?.original_name || ''
|
||||
if (fileName) names.push(files.value)
|
||||
form.value.submit_files = names
|
||||
const list = Array.isArray(form.value.submit_files_list) ? form.value.submit_files_list : []
|
||||
list.push(fileUrl)
|
||||
form.value.submit_files_list = [...list]
|
||||
ElMessage.success(response.message)
|
||||
}
|
||||
|
||||
// 预览项移除(同步预览数组与提交字段)
|
||||
function removeVoucher(index: number) {
|
||||
const list = Array.isArray(form.value.voucher_file_list)
|
||||
? [...form.value.voucher_file_list]
|
||||
: []
|
||||
if (index < 0 || index >= list.length) return
|
||||
list.splice(index, 1)
|
||||
form.value.voucher_file_list = list
|
||||
const names = Array.isArray(form.value.voucher_file) ? [...form.value.voucher_file] : []
|
||||
if (index < names.length) names.splice(index, 1)
|
||||
form.value.voucher_file = names
|
||||
}
|
||||
function removeSubmit(index: number) {
|
||||
const list = Array.isArray(form.value.submit_files_list)
|
||||
? [...form.value.submit_files_list]
|
||||
: []
|
||||
if (index < 0 || index >= list.length) return
|
||||
list.splice(index, 1)
|
||||
form.value.submit_files_list = list
|
||||
const names = Array.isArray(form.value.submit_files) ? [...form.value.submit_files] : []
|
||||
if (index < names.length) names.splice(index, 1)
|
||||
form.value.submit_files = names
|
||||
}
|
||||
</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;
|
||||
}
|
||||
// 创建收款记录 - 弹窗样式对齐设计图
|
||||
.receipt-dialog {
|
||||
.receipt-form {
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.avatarimg {
|
||||
width: 60px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 200px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
:deep(.receipt-dialog .el-dialog__footer) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,710 @@
|
||||
<template>
|
||||
<div class="stocking-page">
|
||||
<div class="stocking-toolbar boxshadow">
|
||||
<div class="toolbar-left">
|
||||
<ElButton
|
||||
type="primary"
|
||||
v-ripple
|
||||
@click="handleCreate"
|
||||
v-if="hasAuth('receipt_records@add')"
|
||||
>
|
||||
创建收款记录
|
||||
</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="created_at" min-width="140" />
|
||||
<ElTableColumn label="付款来源" prop="country_name" min-width="160" />
|
||||
<ElTableColumn label="汇款人" prop="remitter" min-width="120" />
|
||||
<ElTableColumn label="账户" prop="account_name" min-width="120" />
|
||||
<ElTableColumn label="币种" prop="currency_name" min-width="120" />
|
||||
<ElTableColumn label="金额" prop="amount" min-width="120" />
|
||||
<ElTableColumn label="折让金额" prop="discount_amount" min-width="120" />
|
||||
<ElTableColumn label="入账" prop="id" min-width="120">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.received_at">{{ scope.row.received_at }}</span>
|
||||
<el-button v-else type="primary" size="small" @click="handlebtn">确认入账</el-button>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" fixed="right" min-width="140" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow :gutter="10" justify="center">
|
||||
<el-link
|
||||
v-if="hasAuth('receipt_records@show')"
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="handleView(scope.row)"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 查看
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasAuth('receipt_records@update')"
|
||||
type="success"
|
||||
underline="never"
|
||||
@click="handleEdit(scope.row)"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 修改
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="hasAuth('receipt_records@destroy')"
|
||||
type="danger"
|
||||
underline="never"
|
||||
@click="handleDelete()"
|
||||
>
|
||||
<i class="iconfont-sys"></i> 删除
|
||||
</el-link>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<!-- 新建/编辑收款记录 - 通用弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑收款记录' : '创建收款记录'"
|
||||
width="900px"
|
||||
class="receipt-dialog"
|
||||
>
|
||||
<el-form
|
||||
ref="createFormRef"
|
||||
:model="form"
|
||||
:rules="createRules"
|
||||
label-width="180px"
|
||||
class="receipt-form"
|
||||
>
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="付款来源">
|
||||
<el-select v-model="form.country_id" placeholder="请选择付款国家">
|
||||
<el-option
|
||||
v-for="item in countryOptions"
|
||||
:key="item.id"
|
||||
:label="item.cn_addrs"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="汇款人">
|
||||
<el-input v-model="form.remitter" placeholder="请填写汇款人" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="账户">
|
||||
<el-select v-model="form.account_id" placeholder="请选择账户">
|
||||
<el-option
|
||||
v-for="item in accountOptions"
|
||||
:key="item.id"
|
||||
:label="item.short_desc"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="币种">
|
||||
<el-select v-model="form.currency_id" placeholder="请选择币种">
|
||||
<el-option
|
||||
v-for="item in currencyOptions"
|
||||
:key="item.id"
|
||||
:label="item.currency_name"
|
||||
:value="item.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="金额">
|
||||
<el-input v-model="form.amount" placeholder="请填写金额" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="到账时间">
|
||||
<el-date-picker
|
||||
v-model="form.arrived_at"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
type="date"
|
||||
placeholder="请选择到账时间"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="折让金额">
|
||||
<el-input v-model="form.discount_amount" placeholder="请填写折让金额" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="收汇凭证号">
|
||||
<el-input v-model="form.voucher_no" placeholder="请填写收汇凭证号" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="实际汇率(出纳)">
|
||||
<el-input v-model="form.exchange_rate" placeholder="请填写实际汇率" />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="总金额(人民币)实际汇率">
|
||||
<el-input
|
||||
v-model="form.total_amount_cny"
|
||||
placeholder="请填写总金额(人民币)实际汇率"
|
||||
/>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="是否转回内地">
|
||||
<el-radio-group v-model="form.is_domestic_return">
|
||||
<el-radio :label="0">否</el-radio>
|
||||
<el-radio :label="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="外汇到账回单(出纳)">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
:action="actionImageUplode"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="handleUploadSuccess1"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
>
|
||||
<img :src="imageUrl1" alt="" class="avatarimg" />
|
||||
<input type="hidden" v-model="form.fx_receipt_file" />
|
||||
</el-upload>
|
||||
<div
|
||||
v-if="Array.isArray(form.fx_receipt_file_list) && form.fx_receipt_file_list.length"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
<div
|
||||
v-for="(u, i) in form.fx_receipt_file_list"
|
||||
:key="i"
|
||||
style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px"
|
||||
>
|
||||
<img
|
||||
v-if="/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test((u || '').split('?')[0])"
|
||||
:src="u"
|
||||
style="
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
:href="u"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 200px;
|
||||
"
|
||||
>{{ (u || '').split('?')[0].split('/').pop() }}</a
|
||||
>
|
||||
<el-button link type="danger" @click="removeVoucher(i)" style="width: 80px"
|
||||
>移除</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="dialogVisibleshow"
|
||||
title="查看收款记录"
|
||||
width="900px"
|
||||
class="receipt-dialog"
|
||||
>
|
||||
<el-form ref="createFormRef" :model="formshow" label-width="180px" class="receipt-form">
|
||||
<ElRow :gutter="12" class="item-row-wrap">
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="付款来源">
|
||||
<el-input v-model="formshow.country_name" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="汇款人">
|
||||
<el-input v-model="formshow.remitter" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="账户">
|
||||
<el-input v-model="formshow.account_name" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="币种">
|
||||
<el-input v-model="formshow.currency_name" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="金额">
|
||||
<el-input v-model="formshow.amount" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="到账时间">
|
||||
<el-input v-model="formshow.arrived_at" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="折让金额">
|
||||
<el-input v-model="formshow.discount_amount" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="收汇凭证号">
|
||||
<el-input v-model="formshow.voucher_no" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="实际汇率(出纳)">
|
||||
<el-input v-model="formshow.exchange_rate" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="总金额(人民币)实际汇率">
|
||||
<el-input v-model="formshow.total_amount_cny" disabled />
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="是否转回内地">
|
||||
<el-radio-group v-model="formshow.is_domestic_return" disabled>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
<el-radio :label="1">是</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="12">
|
||||
<el-form-item label="外汇到账回单(出纳)">
|
||||
<div
|
||||
v-if="
|
||||
Array.isArray(formshow.fx_receipt_file_list) &&
|
||||
formshow.fx_receipt_file_list.length
|
||||
"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
<div
|
||||
v-for="(u, i) in formshow.fx_receipt_file_list"
|
||||
:key="i"
|
||||
style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px"
|
||||
>
|
||||
<img
|
||||
v-if="/\.(png|jpe?g|gif|webp|bmp|svg)(\?|$)/i.test((u || '').split('?')[0])"
|
||||
:src="u"
|
||||
style="
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
:href="u"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 200px;
|
||||
"
|
||||
>{{ (u || '').split('?')[0].split('/').pop() }}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import { financeService } from '@/api/admin/finance'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import imageUrl1 from '@/assets/img/crm/upimg.png'
|
||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||
const { uploadHeaders } = useUploadHeaders()
|
||||
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload?scene=receipt_records`
|
||||
defineOptions({ name: 'AdminReceiptRecords' })
|
||||
const { hasAuth } = useAuth()
|
||||
const loading = ref(false)
|
||||
const dialogVisibleshow = ref(false)
|
||||
const keyword = ref('')
|
||||
const tableRef = ref()
|
||||
const tableData = ref<any[]>([])
|
||||
const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
//国家
|
||||
const countryOptions = ref<any[]>([])
|
||||
const fetchCountryOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCountrySelect()
|
||||
res.forEach((item: any) => {
|
||||
item.cn_addrs = item.cn_addr + '(' + item.en_addr + ')'
|
||||
})
|
||||
countryOptions.value = res || []
|
||||
} catch {
|
||||
countryOptions.value = []
|
||||
}
|
||||
}
|
||||
//账户
|
||||
const accountOptions = ref<any[]>([])
|
||||
const fetchAccountOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getAccountSelect()
|
||||
accountOptions.value = res || []
|
||||
} catch {
|
||||
accountOptions.value = []
|
||||
}
|
||||
}
|
||||
//币种
|
||||
const currencyOptions = ref<any[]>([])
|
||||
const fetchCurrencyOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCurrencySelect()
|
||||
currencyOptions.value = res || []
|
||||
} catch {
|
||||
currencyOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 审批弹出框相关
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
watch([keyword], () => {
|
||||
pagination.currentPage = 1
|
||||
// 不自动请求,保持与截图行为一致,点击搜索或回车触发
|
||||
})
|
||||
|
||||
const getList = 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.getReceiptList(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 dialogVisible = ref(false)
|
||||
const createLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentId = ref<string | number | null>(null)
|
||||
const createFormRef = ref<FormInstance>()
|
||||
const form = ref({
|
||||
country_id: '',
|
||||
remitter: '',
|
||||
account_id: '',
|
||||
currency_id: '',
|
||||
amount: '',
|
||||
arrived_at: new Date().toISOString().split('T')[0],
|
||||
discount_amount: '',
|
||||
voucher_no: '',
|
||||
total_amount_cny: '',
|
||||
exchange_rate: '',
|
||||
is_domestic_return: 0,
|
||||
fx_receipt_file: '',
|
||||
fx_receipt_file_list: [] as string[]
|
||||
})
|
||||
const createRules: FormRules = {
|
||||
country_id: [{ required: true, message: '请选择付款来源', trigger: 'change' }],
|
||||
remitter: [{ required: true, message: '请输入汇款人', trigger: 'blur' }],
|
||||
account_id: [{ required: true, message: '请选择账户', trigger: 'change' }],
|
||||
currency_id: [{ required: true, message: '请选择币种', trigger: 'change' }]
|
||||
}
|
||||
const formshow = ref({
|
||||
country_name: '',
|
||||
remitter: '',
|
||||
account_name: '',
|
||||
currency_name: '',
|
||||
amount: '',
|
||||
arrived_at: '',
|
||||
discount_amount: '',
|
||||
voucher_no: '',
|
||||
total_amount_cny: '',
|
||||
exchange_rate: '',
|
||||
is_domestic_return: 0,
|
||||
fx_receipt_file: '',
|
||||
fx_receipt_file_list: [] as string[]
|
||||
})
|
||||
const resetCreateForm = () => {
|
||||
Object.assign(form.value, {
|
||||
country_id: '',
|
||||
remitter: '',
|
||||
account_id: '',
|
||||
currency_id: '',
|
||||
amount: '',
|
||||
arrived_at: new Date().toISOString().split('T')[0],
|
||||
discount_amount: '',
|
||||
voucher_no: '',
|
||||
total_amount_cny: '',
|
||||
exchange_rate: '',
|
||||
is_domestic_return: 0,
|
||||
fx_receipt_file: '',
|
||||
fx_receipt_file_list: []
|
||||
})
|
||||
currentId.value = null
|
||||
createFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
resetCreateForm()
|
||||
isEdit.value = false
|
||||
fetchAccountOptions()
|
||||
fetchCurrencyOptions()
|
||||
fetchCountryOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
createFormRef.value?.validate?.(async (valid: boolean) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
createLoading.value = true
|
||||
// TODO: 根据新增/编辑调用对应接口
|
||||
if (isEdit.value) {
|
||||
const formData = { ...form.value }
|
||||
if ('fx_receipt_file_list' in formData) {
|
||||
delete formData.fx_receipt_file_list
|
||||
}
|
||||
const forms = { ...formData, id: currentId.value }
|
||||
const { message } = await financeService.updateReceipt(forms)
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await financeService.addReceipt(form.value)
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getList()
|
||||
} finally {
|
||||
createLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
const handleView = (row?: any) => {
|
||||
const fx_receipt_file_full = []
|
||||
if (row.fx_receipt_file_full) {
|
||||
fx_receipt_file_full.push(row.fx_receipt_file_full)
|
||||
}
|
||||
formshow.value = row
|
||||
formshow.value.fx_receipt_file_list = fx_receipt_file_full
|
||||
dialogVisibleshow.value = true
|
||||
}
|
||||
const handleEdit = (row?: any) => {
|
||||
resetCreateForm()
|
||||
isEdit.value = true
|
||||
currentId.value = row?.id ?? null
|
||||
if (row && typeof row === 'object') {
|
||||
form.value.country_id = row.country_id ?? form.value.country_id
|
||||
form.value.remitter = row.remitter ?? form.value.remitter
|
||||
form.value.account_id = row.account_id ?? form.value.account_id
|
||||
form.value.currency_id = row.currency_id ?? form.value.currency_id
|
||||
form.value.amount = row.amount ?? form.value.amount
|
||||
form.value.arrived_at = row.arrived_at ?? form.value.arrived_at
|
||||
form.value.discount_amount = row.discount_amount ?? form.value.discount_amount
|
||||
form.value.voucher_no = row.voucher_no ?? form.value.voucher_no
|
||||
form.value.total_amount_cny = row.total_amount_cny ?? form.value.total_amount_cny
|
||||
form.value.exchange_rate = row.exchange_rate ?? form.value.exchange_rate
|
||||
form.value.is_domestic_return = row.is_domestic_return ?? form.value.is_domestic_return
|
||||
form.value.fx_receipt_file = row.fx_receipt_file ?? form.value.fx_receipt_file
|
||||
|
||||
const fx_receipt_file_full = []
|
||||
if (row.fx_receipt_file_full) {
|
||||
fx_receipt_file_full.push(row.fx_receipt_file_full)
|
||||
}
|
||||
form.value.fx_receipt_file_list = Array.isArray(fx_receipt_file_full)
|
||||
? fx_receipt_file_full
|
||||
: form.value.fx_receipt_file_list
|
||||
}
|
||||
fetchAccountOptions()
|
||||
fetchCurrencyOptions()
|
||||
fetchCountryOptions()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const handlebtn = () => {
|
||||
ElMessageBox.confirm('你是否确认入账?', '入账', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// financeService.deleteReceipt({ id: currentId.value }).then((res) => {
|
||||
// ElMessage.success(res.message)
|
||||
// getList()
|
||||
// })
|
||||
})
|
||||
}
|
||||
const handleDelete = () => {
|
||||
ElMessageBox.confirm('确定删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
financeService.deleteReceipt({ id: currentId.value }).then((res) => {
|
||||
ElMessage.success(res.message)
|
||||
getList()
|
||||
})
|
||||
})
|
||||
}
|
||||
function handleUploadSuccess1(response: any) {
|
||||
const fileName = response?.data?.file_name || ''
|
||||
const fileUrl = response?.data?.file_url || response?.data?.full_url || response?.url || ''
|
||||
form.value.fx_receipt_file = [form.value.fx_receipt_file, fileName].filter(Boolean).join(',')
|
||||
const list = Array.isArray(form.value.fx_receipt_file_list)
|
||||
? form.value.fx_receipt_file_list
|
||||
: []
|
||||
list.push(fileUrl)
|
||||
form.value.fx_receipt_file_list = [...list]
|
||||
ElMessage.success(response.message)
|
||||
}
|
||||
// 预览项移除(同步预览数组与提交字段)
|
||||
function removeVoucher(index: number) {
|
||||
const list = Array.isArray(form.value.fx_receipt_file_list)
|
||||
? [...form.value.fx_receipt_file_list]
|
||||
: []
|
||||
if (index < 0 || index >= list.length) return
|
||||
list.splice(index, 1)
|
||||
form.value.fx_receipt_file_list = list
|
||||
const names = (form.value.fx_receipt_file || '').split(',').filter(Boolean)
|
||||
if (index < names.length) names.splice(index, 1)
|
||||
form.value.fx_receipt_file = names.join(',')
|
||||
}
|
||||
</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;
|
||||
}
|
||||
// 创建收款记录 - 弹窗样式对齐设计图
|
||||
.receipt-dialog {
|
||||
.receipt-form {
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// justify-content: flex-end;
|
||||
// border: 1px dashed #dcdfe6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.avatarimg {
|
||||
width: 60px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
width: 200px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
:deep(.receipt-dialog .el-dialog__footer) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</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>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,612 @@
|
||||
<template>
|
||||
<div class="page-content user">
|
||||
<div class="content">
|
||||
<div class="left-wrap">
|
||||
<div class="user-wrap box-style">
|
||||
<!-- <img class="bg" src="@imgs/user/bg.webp" /> -->
|
||||
<img
|
||||
class="avatar"
|
||||
:src="userInfo.avatar != '' ? userInfo.avatar : '/src/assets/img/user/avatar.webp'"
|
||||
/>
|
||||
<h2 class="name">{{ userInfo.name }}</h2>
|
||||
<p class="des">SN:{{ userInfo.sn }}</p>
|
||||
<div class="outer-info">
|
||||
<!-- <div class="info-row">
|
||||
<span class="info-label">昵 称:</span>
|
||||
<span class="info-value">{{ userInfo.phone }}</span>
|
||||
</div> -->
|
||||
<div class="info-row">
|
||||
<span class="info-label">电 话:</span>
|
||||
<span class="info-value">{{ userInfo.phone }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">邮 箱:</span>
|
||||
<span class="info-value">{{ userInfo.email }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">公 司:</span>
|
||||
<span class="info-value">{{ userInfo.corp_name }}</span>
|
||||
</div>
|
||||
<!-- <div class="info-row">
|
||||
<span class="info-label">国 家:</span>
|
||||
<span class="info-value">{{ userInfo.country_txt }}</span>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<div class="info box-style">
|
||||
<h1 class="title"><span class="border" style="margin-right: 5px"></span>基本信息</h1>
|
||||
|
||||
<ElForm
|
||||
:model="form"
|
||||
class="form"
|
||||
ref="ruleFormRef"
|
||||
label-width="86px"
|
||||
label-position="left"
|
||||
>
|
||||
<ElFormItem label="头像" prop="avatar">
|
||||
<el-upload
|
||||
class="qrcode-upload"
|
||||
:action="actionImageUplode"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUploadavatar"
|
||||
:disabled="!isEdit"
|
||||
>
|
||||
<div class="qrcode-upload-box">
|
||||
<img
|
||||
class="avatar"
|
||||
:src="
|
||||
imageUrlavatar != '' ? imageUrlavatar : '/src/assets/img/user/avatar.webp'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</el-upload>
|
||||
</ElFormItem>
|
||||
<ElRow>
|
||||
<ElFormItem label="昵称" prop="name">
|
||||
<el-input v-model="form.name" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="性别" prop="sex" class="right-input">
|
||||
<ElSelect v-model="form.sex" placeholder="未知" :disabled="!isEdit">
|
||||
<ElOption
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
|
||||
<ElRow>
|
||||
<ElFormItem label="联系方式" prop="phone">
|
||||
<ElInput v-model="form.phone" disabled />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="公司" prop="company" class="right-input">
|
||||
<ElInput v-model="form.corp_name" disabled />
|
||||
</ElFormItem>
|
||||
<!-- <ElFormItem label="职务" prop="position" class="right-input">
|
||||
<ElInput v-model="form.position" :disabled="!isEdit" />
|
||||
</ElFormItem> -->
|
||||
</ElRow>
|
||||
|
||||
<!-- <ElRow>
|
||||
<ElFormItem label="公司" prop="company">
|
||||
<ElInput v-model="form.corp_name" disabled />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="国家" prop="address" class="right-input">
|
||||
<el-select
|
||||
v-model="form.country_id"
|
||||
filterable
|
||||
placeholder="请选择国家"
|
||||
:disabled="!isEdit"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in countryOptions"
|
||||
:key="item.id"
|
||||
:label="item.cn_addr"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</ElFormItem>
|
||||
</ElRow> -->
|
||||
<ElRow>
|
||||
<ElFormItem label="邮箱" prop="email">
|
||||
<ElInput v-model="form.email" :disabled="!isEdit" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="签名" prop="signature" class="right-input">
|
||||
<template v-if="form.sign_img">
|
||||
<img :src="sign_img_url" class="signature-img" />
|
||||
<el-button
|
||||
v-if="isEdit"
|
||||
style="margin-left: 12px"
|
||||
@click="showReSignQR = !showReSignQR"
|
||||
>
|
||||
重新签名
|
||||
</el-button>
|
||||
<div
|
||||
v-if="isEdit && showReSignQR"
|
||||
class="signature-qrcode"
|
||||
style="margin-top: 20px; margin-left: 20px"
|
||||
>
|
||||
<qrcode-vue
|
||||
:value="qrcodeUrl"
|
||||
:size="200"
|
||||
level="M"
|
||||
render-as="svg"
|
||||
:include-margin="true"
|
||||
background="#ffffff"
|
||||
foreground="#000000"
|
||||
/>
|
||||
<div class="qrcode-tip">手机扫码重新签名</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="signature-qrcode">
|
||||
<qrcode-vue
|
||||
:value="qrcodeUrl"
|
||||
:size="200"
|
||||
level="M"
|
||||
render-as="svg"
|
||||
:include-margin="true"
|
||||
background="#ffffff"
|
||||
foreground="#000000"
|
||||
/>
|
||||
<div class="qrcode-tip">手机扫码进行签名</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- <el-button @click="handleSignature" type="primary">签名</el-button> -->
|
||||
<el-input type="hidden" v-model="form.signature" />
|
||||
</ElFormItem>
|
||||
</ElRow>
|
||||
<!-- <ElRow>
|
||||
<ElFormItem
|
||||
label="二维码"
|
||||
prop="socialize"
|
||||
:class="isEdit ? 'qrcode-radio-group' : ''"
|
||||
>
|
||||
<el-radio-group v-model="form.socialize" v-if="isEdit">
|
||||
<el-radio label="微信">微信</el-radio>
|
||||
<el-radio label="WhatsApp">WhatsApp</el-radio>
|
||||
</el-radio-group>
|
||||
<template v-else>
|
||||
<img
|
||||
v-if="form.socialize_pic_full"
|
||||
:src="form.socialize_pic_full"
|
||||
class="qrcode-img"
|
||||
/>
|
||||
<span v-else class="qrcode-upload-text">暂无二维码</span>
|
||||
</template>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="isEdit" label="上传二维码" prop="address" class="right-input">
|
||||
<el-upload
|
||||
class="qrcode-upload"
|
||||
:action="actionImageUplode"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleUploadavatarqrcode"
|
||||
>
|
||||
<div class="qrcode-upload-box">
|
||||
<img
|
||||
v-if="form.socialize_pic_full"
|
||||
:src="socialize_pic_full"
|
||||
class="qrcode-img"
|
||||
/>
|
||||
<span v-else class="qrcode-upload-text">
|
||||
<img :src="imageUrls" class="qrcode-img" />
|
||||
</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</ElFormItem>
|
||||
</ElRow> -->
|
||||
|
||||
<div class="el-form-item-right">
|
||||
<ElButton type="primary" style="width: 90px" v-ripple @click="edit">
|
||||
<i v-if="!isEdit" class="iconfont-sys"> </i>
|
||||
{{ isEdit ? '保存' : '编辑' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
|
||||
<div class="info box-style" style="margin-top: 20px">
|
||||
<h1 class="title">更改密码</h1>
|
||||
|
||||
<ElForm :model="pwdForm" class="form" label-width="86px" label-position="left">
|
||||
<ElFormItem label="当前密码" prop="password">
|
||||
<ElInput
|
||||
v-model="pwdForm.password"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="新密码" prop="password_new">
|
||||
<ElInput
|
||||
v-model="pwdForm.password_new"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<ElFormItem label="确认新密码" prop="confirmPassword">
|
||||
<ElInput
|
||||
v-model="pwdForm.confirmPassword"
|
||||
type="password"
|
||||
:disabled="!isEditPwd"
|
||||
show-password
|
||||
/>
|
||||
</ElFormItem>
|
||||
|
||||
<div class="el-form-item-right">
|
||||
<ElButton type="primary" style="width: 90px" v-ripple @click="editPwd">
|
||||
<i v-if="!isEditPwd" class="iconfont-sys"> </i>
|
||||
{{ isEditPwd ? '保存' : '编辑' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElForm, FormInstance } from 'element-plus'
|
||||
// import imageUrl from '@/assets/img/crm/upimg.png'
|
||||
import { CrmService } from '@/api/crmApi'
|
||||
import { UserService } from '@/api/usersApi'
|
||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { computed } from 'vue'
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
const { uploadHeaders } = useUploadHeaders()
|
||||
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
|
||||
defineOptions({ name: 'AdminUserCenter' })
|
||||
// 定义客户详情类型
|
||||
interface CustomerInfo {
|
||||
[key: string]: any
|
||||
}
|
||||
// const imageUrls = ref(imageUrl)
|
||||
const imageUrlavatar = ref('')
|
||||
const sign_img_url = ref('')
|
||||
// const socialize_pic_full = ref('')
|
||||
// const countryOptions = ref<any>()
|
||||
const form = ref<CustomerInfo>({})
|
||||
//获取公司 getCompanyList
|
||||
// const companyList = ref<any>()
|
||||
//国家
|
||||
// const getarea = async () => {
|
||||
// try {
|
||||
// // 这里调用获取品牌列表的接口
|
||||
// const res = await CrmService.getCountry()
|
||||
// countryOptions.value = res||[]
|
||||
// } catch (error) { countryOptions.value = [] }
|
||||
// }
|
||||
// const { getUserInfo } = useUserStore()
|
||||
const userInfo = ref<CustomerInfo>({})
|
||||
const getuserInfo = async () => {
|
||||
const data = await UserService.getUserInfo()
|
||||
userInfo.value = data.user
|
||||
form.value = data.user
|
||||
imageUrlavatar.value = data.user.avatar
|
||||
// socialize_pic_full.value = data.socialize_pic_full
|
||||
sign_img_url.value = data.user.sign_img_url
|
||||
// userInfo.value = getUserInfo.user
|
||||
// form.value = getUserInfo.user
|
||||
// imageUrlavatar.value = getUserInfo.user.avatar
|
||||
// sign_img_url.value = getUserInfo.user.sign_img_url
|
||||
// socialize_pic_full.value = data.socialize_pic_full
|
||||
}
|
||||
const isEdit = ref(false)
|
||||
const showReSignQR = ref(false)
|
||||
const isEditPwd = ref(false)
|
||||
const pwdForm = reactive({
|
||||
password: '',
|
||||
password_new: '',
|
||||
confirmPassword: ''
|
||||
})
|
||||
|
||||
const ruleFormRef = ref<FormInstance>()
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: '',
|
||||
label: '未知'
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
label: '男'
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: '女'
|
||||
}
|
||||
]
|
||||
onMounted(async () => {
|
||||
// await getarea()
|
||||
await getuserInfo()
|
||||
})
|
||||
const qrcodeUrl = computed(() => {
|
||||
const userStore = useUserStore() as any
|
||||
const userId = form.value?.id || userInfo.value?.id || ''
|
||||
const base = location.origin
|
||||
const params = new URLSearchParams({ userId: String(userId) })
|
||||
|
||||
// 只保留必要的参数,减少URL长度
|
||||
if (userStore?.accessToken) params.append('token', userStore.accessToken)
|
||||
if (userStore?.companyabbr) params.append('corp', userStore.companyabbr)
|
||||
if (userStore?.companyid) params.append('corpid', userStore.companyid)
|
||||
if (userStore?.systemsabbr) params.append('sys', userStore.systemsabbr)
|
||||
if (userStore?.systemsid) params.append('sysid', userStore.systemsid)
|
||||
if (userStore?.loginType) params.append('type', userStore.loginType)
|
||||
if (userStore?.language) params.append('lang', userStore.language)
|
||||
|
||||
return `${base}/auth/signature/mobile?${params.toString()}`
|
||||
})
|
||||
const edit = async () => {
|
||||
if (isEdit.value) {
|
||||
const payload: any = { ...(form.value as any) }
|
||||
delete payload.sign_img_url
|
||||
const res = await UserService.editUserInfo(payload)
|
||||
ElMessage.success(res?.message || '操作成功')
|
||||
getuserInfo()
|
||||
}
|
||||
isEdit.value = !isEdit.value
|
||||
if (isEdit.value === false) {
|
||||
showReSignQR.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const editPwd = async () => {
|
||||
if (isEditPwd.value) {
|
||||
// 判断新密码和确认密码是否一致
|
||||
if (pwdForm.password_new !== pwdForm.confirmPassword) {
|
||||
ElMessage.error('新密码和确认密码不一致')
|
||||
return
|
||||
}
|
||||
const res = await CrmService.savePassword(pwdForm)
|
||||
ElMessage.success(res?.message || '操作成功')
|
||||
}
|
||||
isEditPwd.value = !isEditPwd.value
|
||||
}
|
||||
//头像上传
|
||||
function handleUploadavatar(response: any) {
|
||||
// 假设返回图片地址字段为 response.url 或 response.data.url
|
||||
imageUrlavatar.value = response.url || (response.data && response.data.file_url) || ''
|
||||
// 如需同步到 form.picture
|
||||
form.value.avatar = response.url || (response.data && response.data.file_name) || ''
|
||||
}
|
||||
//二维码上传
|
||||
// function handleUploadavatarqrcode(response: any) {
|
||||
// // 假设返回图片地址字段为 response.url 或 response.data.url
|
||||
// socialize_pic_full.value = response.url || (response.data && response.data.file_url) || ''
|
||||
// imageUrls.value = response.url || (response.data && response.data.file_url) || ''
|
||||
// // 如需同步到 form.picture
|
||||
// form.value.socialize_pic = response.url || (response.data && response.data.file_name) || ''
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.user {
|
||||
.icon {
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
overflow: hidden;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentcolor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.qrcode-radio-group .el-form-item__content) {
|
||||
height: 40px;
|
||||
}
|
||||
.avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.qrcode-img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.page-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 !important;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
$box-radius: calc(var(--custom-radius) + 4px);
|
||||
|
||||
.box-style {
|
||||
border: 1px solid var(--art-border-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
|
||||
.left-wrap {
|
||||
width: 350px;
|
||||
margin-right: 25px;
|
||||
|
||||
.user-wrap {
|
||||
position: relative;
|
||||
height: 400px;
|
||||
padding: 35px 30px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
// background: var(--art-main-bg-color);
|
||||
background: linear-gradient(180deg, #e9f0ff 0%, #ffffff 67.9%);
|
||||
|
||||
border-radius: $box-radius;
|
||||
.avatar {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
// margin-top: 120px;
|
||||
object-fit: cover;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: 20px;
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.des {
|
||||
margin-top: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.outer-info {
|
||||
// width: 260px;
|
||||
margin: 0 auto;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.info-label {
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
color: #222;
|
||||
text-align: right;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.info-value {
|
||||
color: #333;
|
||||
margin-left: 8px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.lables {
|
||||
margin-top: 40px;
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-top: 15px;
|
||||
|
||||
> div {
|
||||
padding: 3px 6px;
|
||||
margin: 0 10px 10px 0;
|
||||
font-size: 12px;
|
||||
background: var(--art-main-bg-color);
|
||||
border: 1px solid var(--art-border-color);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gallery {
|
||||
margin-top: 25px;
|
||||
border-radius: 10px;
|
||||
|
||||
.item {
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-wrap {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-radius: $box-radius;
|
||||
|
||||
.info {
|
||||
background: var(--art-main-bg-color);
|
||||
border-radius: $box-radius;
|
||||
|
||||
.title {
|
||||
padding: 15px 25px;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
color: var(--art-text-gray-800);
|
||||
// border-bottom: 1px solid var(--art-border-color);
|
||||
}
|
||||
|
||||
.form {
|
||||
box-sizing: border-box;
|
||||
padding: 30px 25px;
|
||||
|
||||
> .el-row {
|
||||
.el-form-item {
|
||||
width: calc(50% - 10px);
|
||||
}
|
||||
|
||||
.el-input,
|
||||
.el-select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.right-input {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.el-form-item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
|
||||
.el-button {
|
||||
width: 110px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $device-ipad-vertical) {
|
||||
.page-content {
|
||||
.content {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
|
||||
.left-wrap {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.right-wrap {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.signature-img {
|
||||
width: 160px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,654 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div v-if="!isMobile" class="dict-layout">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :xs="24" :sm="10" :lg="10">
|
||||
<div class="dict-type-card">
|
||||
<div class="card-header">
|
||||
<div style="display: flex; align-items: center">
|
||||
<ElInput
|
||||
v-model="brandKeyword"
|
||||
placeholder="搜索品牌名称/编码"
|
||||
clearable
|
||||
@keyup.enter="getBrandList"
|
||||
/>
|
||||
<ElButton style="margin-left: 8px" @click="getBrandList" v-ripple>搜索</ElButton>
|
||||
</div>
|
||||
<ElButton
|
||||
type="primary"
|
||||
style="margin-left: 8px"
|
||||
@click="showBrandDialog('add')"
|
||||
v-if="hasAuth('brand@add')"
|
||||
>新增品牌</ElButton
|
||||
>
|
||||
</div>
|
||||
<el-table
|
||||
:data="brandTableData"
|
||||
v-loading="brandLoading"
|
||||
height="calc(100vh - 280px)"
|
||||
@row-click="handleBrandRowClick"
|
||||
:highlight-current-row="true"
|
||||
:row-key="'id'"
|
||||
>
|
||||
<el-table-column prop="brand_code" label="品牌编码" />
|
||||
<el-table-column prop="brand_name" label="品牌名称" />
|
||||
<el-table-column label="Logo" width="80">
|
||||
<template #default="scope">
|
||||
<div class="brand-logo-cell">
|
||||
<img
|
||||
v-if="scope.row.logo_full"
|
||||
:src="scope.row.logo_full"
|
||||
:alt="scope.row.brand_name"
|
||||
/>
|
||||
<div v-else class="logo-placeholder">{{ scope.row.brand_name?.charAt(0) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="description" label="描述" show-overflow-tooltip /> -->
|
||||
<!-- <el-table-column prop="sort" label="排序" width="80" /> -->
|
||||
<el-table-column label="是否热门" prop="hot">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.hot == 1 ? 'danger' : 'info'">
|
||||
{{ scope.row.hot == 1 ? '是' : '否' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'success' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '停用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" min-width="140" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
size="small"
|
||||
@click.stop="showBrandDialog('edit', scope.row)"
|
||||
v-if="hasAuth('brand@update')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click.stop="handleBrandDelete(scope.row)"
|
||||
v-if="hasAuth('brand@destroy')"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<template #empty>
|
||||
<el-empty description="暂无品牌" v-show="!brandLoading" />
|
||||
</template>
|
||||
</el-table>
|
||||
<div class="type-pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, prev, pager, next"
|
||||
:current-page="brandPagination.currentPage"
|
||||
:page-size="brandPagination.pageSize"
|
||||
:total="brandPagination.total"
|
||||
:page-sizes="[10, 20, 30, 50]"
|
||||
:pager-count="4"
|
||||
@current-change="handleBrandCurrentChange"
|
||||
@size-change="handleBrandSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :xs="24" :sm="14" :lg="14">
|
||||
<div class="dict-item-card">
|
||||
<div class="card-header">
|
||||
<div class="left">
|
||||
<span v-if="activeBrand"
|
||||
>当前品牌:{{ activeBrand.brand_name }}({{ activeBrand.brand_code }})</span
|
||||
>
|
||||
<span v-else class="muted">请选择左侧车辆品牌</span>
|
||||
<ElButton
|
||||
type="primary"
|
||||
style="margin-left: 8px"
|
||||
@click="showSeriesDialog('add')"
|
||||
:disabled="!activeBrand"
|
||||
v-if="hasAuth('series@add')"
|
||||
>新增系列</ElButton
|
||||
>
|
||||
</div>
|
||||
<div class="right">
|
||||
<ElInput
|
||||
v-model="seriesKeyword"
|
||||
placeholder="搜索系列名称/编码"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@keyup.enter="getSeriesList"
|
||||
/>
|
||||
<ElButton style="margin-left: 8px" @click="getSeriesList" v-ripple>搜索</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<ArtTable
|
||||
ref="seriesTableRef"
|
||||
row-key="id"
|
||||
:loading="seriesLoading"
|
||||
:data="seriesTableData"
|
||||
:currentPage="seriesPagination.currentPage"
|
||||
:pageSize="seriesPagination.pageSize"
|
||||
:total="seriesPagination.total"
|
||||
@size-change="handleSeriesSizeChange"
|
||||
@current-change="handleSeriesCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn label="系列编码" prop="series_code" min-width="120" />
|
||||
<ElTableColumn label="系列名称" prop="series_name" min-width="140" />
|
||||
<ElTableColumn
|
||||
label="描述"
|
||||
prop="description"
|
||||
min-width="160"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<!-- <ElTableColumn label="排序" prop="sort" width="80" /> -->
|
||||
<ElTableColumn label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'success' : 'info'">{{
|
||||
scope.row.status ? '启用' : '停用'
|
||||
}}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn fixed="right" label="操作" min-width="140px" align="center">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showSeriesDialog('edit', scope.row)"
|
||||
v-if="hasAuth('series@update')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="handleSeriesDelete(scope.row)"
|
||||
v-if="hasAuth('series@destroy')"
|
||||
>删除</el-button
|
||||
>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<ElDialog
|
||||
v-model="brandDialogVisible"
|
||||
:title="brandDialogType === 'add' ? '新增品牌' : '编辑品牌'"
|
||||
width="750px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="brandFormRef" :model="brandForm" :rules="brandRules" label-width="100px">
|
||||
<ElFormItem label="品牌编码" prop="brand_code">
|
||||
<ElInput v-model="brandForm.brand_code" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="品牌名称" prop="brand_name">
|
||||
<ElInput v-model="brandForm.brand_name" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="品牌Logo" prop="logo">
|
||||
<ElUpload
|
||||
class="logo-uploader"
|
||||
:action="actionImageUplode"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleLogoSuccess"
|
||||
:before-upload="beforeLogoUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img v-if="brandForm.logo" :src="logourl" class="logo-preview" />
|
||||
<ElIcon v-else class="logo-uploader-icon"><Plus /></ElIcon>
|
||||
</ElUpload>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="是否热门" prop="hot">
|
||||
<el-radio-group v-model="brandForm.hot">
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="品牌描述" prop="description">
|
||||
<ElInput v-model="brandForm.description" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="brandForm.sort" :min="0" :max="999" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态" prop="status">
|
||||
<ElSwitch v-model="brandForm.status" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="brandDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleBrandSubmit(brandFormRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog
|
||||
v-model="seriesDialogVisible"
|
||||
:title="seriesDialogType === 'add' ? '新增系列' : '编辑系列'"
|
||||
width="750px"
|
||||
align-center
|
||||
>
|
||||
<ElForm ref="seriesFormRef" :model="seriesForm" :rules="seriesRules" label-width="100px">
|
||||
<ElFormItem label="所属品牌">
|
||||
<ElInput
|
||||
:model-value="
|
||||
activeBrand ? `${activeBrand.brand_name}(${activeBrand.brand_code})` : ''
|
||||
"
|
||||
disabled
|
||||
/>
|
||||
</ElFormItem>
|
||||
<!-- <ElFormItem label="系列编码" prop="series_code">
|
||||
<ElInput v-model="seriesForm.series_code" />
|
||||
</ElFormItem> -->
|
||||
<ElFormItem label="系列名称" prop="series_name">
|
||||
<ElInput v-model="seriesForm.series_name" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="系列描述" prop="description">
|
||||
<ElInput v-model="seriesForm.description" type="textarea" :rows="3" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="seriesForm.sort" :min="0" :max="999" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态" prop="status">
|
||||
<ElSwitch v-model="seriesForm.status" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="seriesDialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSeriesSubmit(seriesFormRef)">提交</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 { Plus } from '@element-plus/icons-vue'
|
||||
import { vehicleService } from '@/api/vehicleApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { useUploadHeaders } from '@/composables/useUploadHeaders'
|
||||
const { uploadHeaders } = useUploadHeaders()
|
||||
let actionImageUplode = `${import.meta.env.VITE_API_URL}/ticket/upload`
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'AdminVehicleBrand' })
|
||||
const { width } = useWindowSize()
|
||||
const isMobile = computed(() => width.value < 500)
|
||||
const logourl = ref('')
|
||||
// 左侧 品牌
|
||||
const brandLoading = ref(false)
|
||||
const brandKeyword = ref<string>('')
|
||||
const brandTableData = ref<any[]>([])
|
||||
const brandPagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
const activeBrand = ref<any | null>(null)
|
||||
|
||||
const brandFormRef = ref<FormInstance>()
|
||||
const brandDialogVisible = ref(false)
|
||||
const brandDialogType = ref<'add' | 'edit'>('add')
|
||||
const brandForm = reactive<any>({
|
||||
id: '',
|
||||
brand_code: '',
|
||||
brand_name: '',
|
||||
logo: '',
|
||||
hot: 0,
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
const brandRules = reactive<FormRules>({
|
||||
brand_code: [{ required: true, message: '请输入品牌编码', trigger: 'blur' }],
|
||||
brand_name: [{ required: true, message: '请输入品牌名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const getBrandList = async () => {
|
||||
brandLoading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = brandPagination
|
||||
const { records, current, size, total } = await vehicleService.getBrandList({
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: brandKeyword.value
|
||||
})
|
||||
brandTableData.value = records
|
||||
Object.assign(brandPagination, { currentPage: current, pageSize: size, total })
|
||||
if (!activeBrand.value && records && records.length) {
|
||||
activeBrand.value = records[0]
|
||||
await getSeriesList()
|
||||
}
|
||||
} finally {
|
||||
brandLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleBrandRowClick = async (row: any) => {
|
||||
activeBrand.value = row
|
||||
seriesPagination.currentPage = 1
|
||||
await getSeriesList()
|
||||
}
|
||||
const handleBrandSizeChange = (val: number) => {
|
||||
brandPagination.pageSize = val
|
||||
getBrandList()
|
||||
}
|
||||
const handleBrandCurrentChange = (val: number) => {
|
||||
brandPagination.currentPage = val
|
||||
getBrandList()
|
||||
}
|
||||
|
||||
const showBrandDialog = (type: 'add' | 'edit', row?: any) => {
|
||||
brandDialogVisible.value = true
|
||||
brandDialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
logourl.value = row.logo_full
|
||||
Object.assign(brandForm, {
|
||||
id: row.id,
|
||||
brand_code: row.brand_code,
|
||||
brand_name: row.brand_name,
|
||||
hot: row.hot,
|
||||
logo: row.logo,
|
||||
description: row.description,
|
||||
sort: row.sort ?? 0,
|
||||
status: row.status ?? 1
|
||||
})
|
||||
} else {
|
||||
Object.assign(brandForm, {
|
||||
id: '',
|
||||
brand_code: '',
|
||||
brand_name: '',
|
||||
logo: '',
|
||||
hot: 0,
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleBrandDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该品牌吗?删除后其系列也可能被清理。', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await vehicleService.brandDelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
if (activeBrand.value && activeBrand.value.id === row.id) {
|
||||
activeBrand.value = null
|
||||
seriesTableData.value = []
|
||||
seriesPagination.total = 0
|
||||
}
|
||||
getBrandList()
|
||||
})
|
||||
}
|
||||
const handleBrandSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (brandDialogType.value === 'add') {
|
||||
const { message } = await vehicleService.brandAdd({
|
||||
brand_code: brandForm.brand_code,
|
||||
brand_name: brandForm.brand_name,
|
||||
logo: brandForm.logo,
|
||||
hot: brandForm.hot,
|
||||
description: brandForm.description,
|
||||
sort: brandForm.sort,
|
||||
status: brandForm.status
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await vehicleService.brandEdit(brandForm)
|
||||
ElMessage.success(message)
|
||||
}
|
||||
brandDialogVisible.value = false
|
||||
getBrandList()
|
||||
})
|
||||
}
|
||||
|
||||
// 右侧 系列
|
||||
const seriesLoading = ref(false)
|
||||
const seriesKeyword = ref<string>('')
|
||||
const seriesTableData = ref<any[]>([])
|
||||
const seriesTableRef = ref()
|
||||
const seriesPagination = reactive({ currentPage: 1, pageSize: 10, total: 0 })
|
||||
|
||||
const seriesFormRef = ref<FormInstance>()
|
||||
const seriesDialogVisible = ref(false)
|
||||
const seriesDialogType = ref<'add' | 'edit'>('add')
|
||||
const seriesForm = reactive<any>({
|
||||
series_code: '',
|
||||
series_name: '',
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
const seriesRules = reactive<FormRules>({
|
||||
series_name: [{ required: true, message: '请输入系列名称', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const getSeriesList = async () => {
|
||||
if (!activeBrand.value) return
|
||||
seriesLoading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = seriesPagination
|
||||
const { records, current, size, total } = await vehicleService.getSeriesList({
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: seriesKeyword.value,
|
||||
brand_id: activeBrand.value.id
|
||||
})
|
||||
seriesTableData.value = records
|
||||
Object.assign(seriesPagination, { currentPage: current, pageSize: size, total })
|
||||
} finally {
|
||||
seriesLoading.value = false
|
||||
}
|
||||
}
|
||||
const handleSeriesSizeChange = (val: number) => {
|
||||
seriesPagination.pageSize = val
|
||||
getSeriesList()
|
||||
}
|
||||
const handleSeriesCurrentChange = (val: number) => {
|
||||
seriesPagination.currentPage = val
|
||||
getSeriesList()
|
||||
}
|
||||
const showSeriesDialog = (type: 'add' | 'edit', row?: any) => {
|
||||
seriesDialogVisible.value = true
|
||||
seriesDialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(seriesForm, {
|
||||
id: row.id,
|
||||
series_code: row.series_code,
|
||||
series_name: row.series_name,
|
||||
description: row.description,
|
||||
sort: row.sort ?? 0,
|
||||
status: row.status ?? 1
|
||||
})
|
||||
} else {
|
||||
Object.assign(seriesForm, {
|
||||
id: '',
|
||||
series_code: '',
|
||||
series_name: '',
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleSeriesDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该系列吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
const { message } = await vehicleService.seriesDelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getSeriesList()
|
||||
})
|
||||
}
|
||||
const handleSeriesSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl || !activeBrand.value) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (seriesDialogType.value === 'add') {
|
||||
const { message } = await vehicleService.seriesAdd({
|
||||
brand_id: activeBrand.value.id,
|
||||
series_name: seriesForm.series_name,
|
||||
description: seriesForm.description,
|
||||
sort: seriesForm.sort,
|
||||
status: seriesForm.status
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await vehicleService.seriesEdit({
|
||||
id: seriesForm.id,
|
||||
brand_id: activeBrand.value.id,
|
||||
series_name: seriesForm.series_name,
|
||||
description: seriesForm.description,
|
||||
sort: seriesForm.sort,
|
||||
status: seriesForm.status
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
seriesDialogVisible.value = false
|
||||
getSeriesList()
|
||||
})
|
||||
}
|
||||
|
||||
// Logo上传处理
|
||||
const handleLogoSuccess = (response: any) => {
|
||||
brandForm.logo = response.url || (response.data && response.data.file_name) || ''
|
||||
// 如需同步到 form.picture
|
||||
logourl.value = response.url || (response.data && response.data.file_url) || ''
|
||||
ElMessage.success('Logo上传成功')
|
||||
}
|
||||
|
||||
const beforeLogoUpload = (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
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getBrandList()
|
||||
})
|
||||
</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;
|
||||
}
|
||||
}
|
||||
|
||||
.brand-logo-cell {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.logo-placeholder {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--el-color-primary-light-8);
|
||||
color: var(--el-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
}
|
||||
|
||||
.logo-preview {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,356 @@
|
||||
<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>
|
||||
<el-form-item label="类型:" label-width="50px">
|
||||
<el-select v-model="typeFilter" placeholder="全部类型" clearable style="min-width: 120px">
|
||||
<el-option label="外观颜色" :value="1" />
|
||||
<el-option label="内饰颜色" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态:" label-width="50px">
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
placeholder="全部状态"
|
||||
clearable
|
||||
style="min-width: 120px"
|
||||
>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="停用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getColorList" v-ripple>搜索</el-button>
|
||||
<el-button @click="resetSearch" 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('color@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="color_code" min-width="120" /> -->
|
||||
<ElTableColumn label="颜色名称" prop="color_text" min-width="150" />
|
||||
<ElTableColumn label="类型" prop="type" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.type === 1 ? 'primary' : 'success'">
|
||||
{{ scope.row.type === 1 ? '外观颜色' : '内饰颜色' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="排序" prop="sort" width="80" />
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'success' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '停用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
|
||||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button
|
||||
link
|
||||
@click="showDialog('edit', scope.row)"
|
||||
v-if="hasAuth('color@update')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteColor(scope.row)"
|
||||
v-if="hasAuth('color@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="color_code" v-if="dialogType === 'edit'">
|
||||
<ElInput v-model="form.color_code" disabled placeholder="请输入颜色编码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="颜色名称" prop="color_text">
|
||||
<ElInput v-model="form.color_text" placeholder="请输入颜色名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="类型" prop="type">
|
||||
<el-radio-group v-model="form.type">
|
||||
<el-radio :label="1">外观颜色</el-radio>
|
||||
<el-radio :label="2">内饰颜色</el-radio>
|
||||
</el-radio-group>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="form.sort" :min="0" :max="999" 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { vehicleService } from '@/api/vehicleApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'AdminVehicleColor' })
|
||||
|
||||
// 响应式数据
|
||||
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>({
|
||||
id: '',
|
||||
color_code: '',
|
||||
color_text: '',
|
||||
type: 1,
|
||||
status: 1,
|
||||
sort: 0
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
color_text: [{ required: true, message: '请输入颜色名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择颜色类型', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 分页数据
|
||||
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 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 vehicleService.getColorList(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) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
// 编辑模式
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
color_code: row.color_code,
|
||||
color_text: row.color_text,
|
||||
type: row.type,
|
||||
status: row.status,
|
||||
sort: row.sort || 0
|
||||
})
|
||||
} else {
|
||||
// 新增模式
|
||||
Object.assign(form, {
|
||||
id: '',
|
||||
color_code: '',
|
||||
color_text: '',
|
||||
type: 1,
|
||||
status: 1,
|
||||
sort: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除颜色
|
||||
const deleteColor = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该颜色吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const { message } = await vehicleService.colorDelete({ 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 {
|
||||
if (dialogType.value === 'add') {
|
||||
// 新增
|
||||
const { message } = await vehicleService.colorAdd({
|
||||
color_text: form.color_text,
|
||||
type: form.type,
|
||||
status: form.status,
|
||||
sort: form.sort
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
// 编辑
|
||||
const { message } = await vehicleService.colorEdit({
|
||||
id: form.id,
|
||||
color_text: form.color_text,
|
||||
type: form.type,
|
||||
status: form.status,
|
||||
sort: form.sort
|
||||
})
|
||||
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,356 @@
|
||||
<template>
|
||||
<div class="modification-page">
|
||||
<div class="modification-toolbar boxshadow">
|
||||
<div class="modification-search-group">
|
||||
<el-form-item label="关键字:" label-width="70px">
|
||||
<el-input
|
||||
style="min-width: 120px"
|
||||
v-model="keyword"
|
||||
placeholder="请输入关键字"
|
||||
clearable
|
||||
@keyup.enter="getModificationList"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getModificationList" 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('modification@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="code" min-width="120" /> -->
|
||||
<ElTableColumn label="项目名称" prop="name" min-width="150" />
|
||||
<ElTableColumn label="改装类型" prop="type" width="120">
|
||||
<template #default="scope">
|
||||
<ElTag :type="getTypeTagType(scope.row.type)">
|
||||
{{ getTypeLabel(scope.row.type) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="排序" prop="sort" width="80" />
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'success' : 'info'">
|
||||
{{ scope.row.status ? '启用' : '停用' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
|
||||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showDialog('edit', scope.row)">编辑</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deleteModification(scope.row)"
|
||||
v-if="hasAuth('modification@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="code" v-if="dialogType === 'edit'">
|
||||
<ElInput v-model="form.code" disabled placeholder="请输入改装项目编码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="项目名称" prop="name">
|
||||
<ElInput v-model="form.name" placeholder="请输入改装项目名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="改装类型" prop="type">
|
||||
<el-select v-model="form.type" placeholder="请选择改装类型" style="width: 100%">
|
||||
<el-option label="外观改装" :value="1" />
|
||||
<el-option label="性能改装" :value="2" />
|
||||
<el-option label="内饰改装" :value="3" />
|
||||
<el-option label="电子设备" :value="4" />
|
||||
<el-option label="其他" :value="5" />
|
||||
</el-select>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="form.sort" :min="0" :max="999" 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { vehicleService } from '@/api/vehicleApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'AdminVehicleModification' })
|
||||
|
||||
// 响应式数据
|
||||
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>({
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
type: 1,
|
||||
status: 1,
|
||||
sort: 0
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入改装项目名称', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '请选择改装类型', trigger: 'change' }]
|
||||
})
|
||||
|
||||
// 分页数据
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
getModificationList()
|
||||
})
|
||||
|
||||
// 监听搜索条件变化
|
||||
watch([keyword, typeFilter, statusFilter], () => {
|
||||
pagination.currentPage = 1
|
||||
getModificationList()
|
||||
})
|
||||
|
||||
// 获取改装项目列表
|
||||
const getModificationList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
}
|
||||
|
||||
const { records, current, size, total } = await vehicleService.getModificationList(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
|
||||
getModificationList()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getModificationList()
|
||||
}
|
||||
|
||||
// 显示对话框
|
||||
const showDialog = (type: string, row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
// 编辑模式
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
status: row.status,
|
||||
sort: row.sort || 0
|
||||
})
|
||||
} else {
|
||||
// 新增模式
|
||||
Object.assign(form, {
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
type: 1,
|
||||
status: 1,
|
||||
sort: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 删除改装项目
|
||||
const deleteModification = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该改装项目吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const { message } = await vehicleService.modificationDelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getModificationList()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
// 新增
|
||||
const { message } = await vehicleService.modificationAdd({
|
||||
name: form.name,
|
||||
type: form.type,
|
||||
status: form.status,
|
||||
sort: form.sort
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
// 编辑
|
||||
const { message } = await vehicleService.modificationEdit({
|
||||
id: form.id,
|
||||
name: form.name,
|
||||
type: form.type,
|
||||
status: form.status,
|
||||
sort: form.sort
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getModificationList()
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取类型标签样式
|
||||
const getTypeTagType = (type: number): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
|
||||
const typeMap: Record<number, 'primary' | 'success' | 'warning' | 'info' | 'danger'> = {
|
||||
1: 'primary', // 外观改装
|
||||
2: 'success', // 性能改装
|
||||
3: 'warning', // 内饰改装
|
||||
4: 'info', // 电子设备
|
||||
5: 'danger' // 其他
|
||||
}
|
||||
return typeMap[type] || 'info'
|
||||
}
|
||||
|
||||
// 获取类型标签文本
|
||||
const getTypeLabel = (type: number) => {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: '外观改装',
|
||||
2: '性能改装',
|
||||
3: '内饰改装',
|
||||
4: '电子设备',
|
||||
5: '其他'
|
||||
}
|
||||
return typeMap[type] || '未知类型'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modification-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.modification-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
|
||||
.modification-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,362 @@
|
||||
<template>
|
||||
<div class="port-page">
|
||||
<div class="port-toolbar boxshadow">
|
||||
<div class="toolbar-left">
|
||||
<ElButton @click="showDialog('add')" v-ripple v-if="hasAuth('port@add')" type="primary"
|
||||
>创建港口</ElButton
|
||||
>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@keyup.enter="getPortList"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="clickable" @click="getPortList"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxshadow">
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:currentPage="pagination.currentPage"
|
||||
:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<!-- <ElTableColumn label="港口代码" prop="port_code" min-width="120" /> -->
|
||||
<ElTableColumn label="港口名称" prop="port_name" min-width="150" />
|
||||
<ElTableColumn label="英文名称" prop="port_name_en" min-width="150" />
|
||||
<ElTableColumn label="国家" prop="country_id" min-width="140">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.country_name }}</span>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<!-- <ElTableColumn label="纬度" prop="lat" min-width="100" /> -->
|
||||
<!-- <ElTableColumn label="经度" prop="lng" min-width="100" /> -->
|
||||
<ElTableColumn label="排序" prop="sort" width="80" />
|
||||
<ElTableColumn label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<ElTag :type="scope.row.status ? 'success' : 'info'">{{
|
||||
scope.row.status ? '启用' : '停用'
|
||||
}}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="创建时间" prop="created_at" min-width="160" />
|
||||
<ElTableColumn fixed="right" label="操作" width="200px">
|
||||
<template #default="scope">
|
||||
<ElRow>
|
||||
<el-button link @click="showDialog('edit', scope.row)" v-if="hasAuth('port@update')"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="deletePort(scope.row)"
|
||||
v-if="hasAuth('port@destroy')"
|
||||
>删除</el-button
|
||||
>
|
||||
</ElRow>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</div>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogType === 'add' ? '新增港口' : '编辑港口'"
|
||||
align-center
|
||||
width="720px"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px" class="form">
|
||||
<ElFormItem label="港口代码" prop="port_code">
|
||||
<ElInput v-model="form.port_code" placeholder="请输入港口代码" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="港口名称" prop="port_name">
|
||||
<ElInput v-model="form.port_name" placeholder="请输入港口名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="英文名称" prop="port_name_en">
|
||||
<ElInput v-model="form.port_name_en" placeholder="请输入港口英文名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="国家" prop="country_id">
|
||||
<el-select
|
||||
v-model="form.country_id"
|
||||
filterable
|
||||
placeholder="请选择国家"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in countryOptions"
|
||||
:key="item.id"
|
||||
:label="item.cn_addr"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</ElFormItem>
|
||||
<!-- <ElFormItem label="纬度" prop="lat">
|
||||
<ElInput v-model="form.lat" placeholder="请输入纬度" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="经度" prop="lng">
|
||||
<ElInput v-model="form.lng" placeholder="请输入经度" />
|
||||
</ElFormItem> -->
|
||||
<ElFormItem label="排序" prop="sort">
|
||||
<ElInputNumber v-model="form.sort" :min="0" :max="9999" style="width: 100%" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="港口描述" prop="description">
|
||||
<ElInput
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="300"
|
||||
show-word-limit
|
||||
placeholder="请输入港口描述"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="状态">
|
||||
<ElSwitch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" @click="handleSubmit(formRef)">提交</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { vehicleService } from '@/api/vehicleApi'
|
||||
import { commonService } from '@/api/commonApi'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
const { hasAuth } = useAuth()
|
||||
defineOptions({ name: 'AdminVehiclePort' })
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
const formRef = ref<FormInstance>()
|
||||
const keyword = ref<string>('')
|
||||
const countryFilter = ref<number | undefined>(undefined)
|
||||
const statusFilter = ref<number | undefined>(undefined)
|
||||
|
||||
const countryOptions = ref<any[]>([])
|
||||
const loadCountryOptions = async () => {
|
||||
try {
|
||||
const res = await commonService.getCountrySelect()
|
||||
countryOptions.value = res || []
|
||||
} catch (e) {
|
||||
countryOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const form = reactive<any>({
|
||||
id: '',
|
||||
status: 1,
|
||||
sort: 0,
|
||||
port_code: '',
|
||||
port_name: '',
|
||||
port_name_en: '',
|
||||
lat: '',
|
||||
lng: '',
|
||||
description: '',
|
||||
country_id: undefined
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
port_code: [{ required: true, message: '请输入港口代码', trigger: 'blur' }],
|
||||
port_name: [{ required: true, message: '请输入港口名称', trigger: 'blur' }],
|
||||
country_id: [{ required: true, message: '请选择国家', trigger: 'change' }]
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
const tableData = ref<any[]>([])
|
||||
const tableRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
loadCountryOptions()
|
||||
getPortList()
|
||||
})
|
||||
watch([keyword, countryFilter, statusFilter], () => {
|
||||
pagination.currentPage = 1
|
||||
getPortList()
|
||||
})
|
||||
|
||||
const getPortList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const { currentPage, pageSize } = pagination
|
||||
const params = {
|
||||
current: currentPage,
|
||||
size: pageSize,
|
||||
keyword: keyword.value
|
||||
}
|
||||
const { records, current, size, total } = await vehicleService.getPortList(params)
|
||||
tableData.value = records || []
|
||||
Object.assign(pagination, { currentPage: current, pageSize: size, total })
|
||||
} catch (error) {
|
||||
console.error('获取港口列表失败:', error)
|
||||
ElMessage.error('获取港口列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSizeChange = (newPageSize: number) => {
|
||||
pagination.pageSize = newPageSize
|
||||
getPortList()
|
||||
}
|
||||
const handleCurrentChange = (newCurrentPage: number) => {
|
||||
pagination.currentPage = newCurrentPage
|
||||
getPortList()
|
||||
}
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: any) => {
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
if (type === 'edit' && row) {
|
||||
Object.assign(form, {
|
||||
id: row.id,
|
||||
status: row.status,
|
||||
sort: row.sort ?? 0,
|
||||
port_code: row.port_code,
|
||||
port_name: row.port_name,
|
||||
port_name_en: row.port_name_en,
|
||||
lat: row.lat,
|
||||
lng: row.lng,
|
||||
description: row.description,
|
||||
country_id: row.country_id
|
||||
})
|
||||
} else {
|
||||
Object.assign(form, {
|
||||
id: '',
|
||||
status: 1,
|
||||
sort: 0,
|
||||
port_code: '',
|
||||
port_name: '',
|
||||
port_name_en: '',
|
||||
lat: '',
|
||||
lng: '',
|
||||
description: '',
|
||||
country_id: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const deletePort = (row: any) => {
|
||||
ElMessageBox.confirm('确定删除该港口吗?', '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'error'
|
||||
}).then(async () => {
|
||||
try {
|
||||
const { message } = await vehicleService.portDelete({ id: row.id })
|
||||
ElMessage.success(message)
|
||||
getPortList()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
if (dialogType.value === 'add') {
|
||||
const { message } = await vehicleService.portAdd({
|
||||
status: form.status,
|
||||
sort: form.sort,
|
||||
port_code: form.port_code,
|
||||
port_name: form.port_name,
|
||||
port_name_en: form.port_name_en,
|
||||
lat: form.lat,
|
||||
lng: form.lng,
|
||||
description: form.description,
|
||||
country_id: form.country_id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
} else {
|
||||
const { message } = await vehicleService.portEdit({
|
||||
id: form.id,
|
||||
status: form.status,
|
||||
sort: form.sort,
|
||||
port_code: form.port_code,
|
||||
port_name: form.port_name,
|
||||
port_name_en: form.port_name_en,
|
||||
lat: form.lat,
|
||||
lng: form.lng,
|
||||
description: form.description,
|
||||
country_id: form.country_id
|
||||
})
|
||||
ElMessage.success(message)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
getPortList()
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getCountryLabel = (id: number | undefined) => {
|
||||
if (!id) return '-'
|
||||
const found = countryOptions.value.find((c: any) => c.id === id)
|
||||
return found ? found.name : id
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-table__empty-block) {
|
||||
height: 100% !important;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.port-page {
|
||||
background: var(--art-main-bg-color);
|
||||
min-height: 100vh;
|
||||
}
|
||||
.port-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 20px 20px;
|
||||
background: var(--art-main-bg-color);
|
||||
}
|
||||
.port-search-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
.el-col2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user