项目+聊天基本功能
This commit is contained in:
113
package-lock.json
generated
113
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@types/socket.io-client": "^3.0.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.6.2",
|
||||
@@ -17,6 +18,7 @@
|
||||
"element-plus": "^2.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vue": "^3.3.11",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.5"
|
||||
@@ -1124,6 +1126,11 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/@transloadit/prettier-bytes": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
|
||||
@@ -1167,6 +1174,15 @@
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/socket.io-client": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-3.0.0.tgz",
|
||||
"integrity": "sha512-s+IPvFoEIjKA3RdJz/Z2dGR4gLgysKi8owcnrVwNjgvc01Lk68LJDDsG2GRqegFITcxmvCMYM7bhMpwEMlHmDg==",
|
||||
"deprecated": "This is a stub types definition. socket.io-client provides its own type definitions, so you do not need this installed.",
|
||||
"dependencies": {
|
||||
"socket.io-client": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
|
||||
@@ -1450,7 +1466,6 @@
|
||||
"version": "5.1.23",
|
||||
"resolved": "https://registry.npmjs.org/@wangeditor/editor/-/editor-5.1.23.tgz",
|
||||
"integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@uppy/core": "^2.1.1",
|
||||
"@uppy/xhr-upload": "^2.0.3",
|
||||
@@ -1479,7 +1494,6 @@
|
||||
"version": "5.1.12",
|
||||
"resolved": "https://registry.npmjs.org/@wangeditor/editor-for-vue/-/editor-for-vue-5.1.12.tgz",
|
||||
"integrity": "sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@wangeditor/editor": ">=5.1.0",
|
||||
"vue": "^3.0.5"
|
||||
@@ -1741,6 +1755,22 @@
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -1878,6 +1908,26 @@
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
@@ -2456,6 +2506,11 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/namespace-emitter": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
|
||||
@@ -2735,6 +2790,32 @@
|
||||
"node": ">=12.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -2998,6 +3079,34 @@
|
||||
"integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@types/socket.io-client": "^3.0.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.6.2",
|
||||
@@ -17,6 +18,7 @@
|
||||
"element-plus": "^2.4.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^2.1.7",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"vue": "^3.3.11",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.5"
|
||||
|
||||
11
src/api/group.js
Normal file
11
src/api/group.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import {groupRequest} from "@/utils/api";
|
||||
|
||||
// 群聊相关API
|
||||
export const groupAPI = {
|
||||
// 登录
|
||||
list: (params) => groupRequest.get('/group/list', {params})
|
||||
}
|
||||
|
||||
export default {
|
||||
groupAPI
|
||||
}
|
||||
12
src/api/message.js
Normal file
12
src/api/message.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import {groupRequest, messageRequest} from "@/utils/api";
|
||||
|
||||
// 群聊相关API
|
||||
export const messageAPI = {
|
||||
// 登录
|
||||
list: (params) => messageRequest.get('/message/list', {params}),
|
||||
read: (data) => groupRequest.post('/message/read', data),
|
||||
}
|
||||
|
||||
export default {
|
||||
messageAPI
|
||||
}
|
||||
1
src/assets/svg/chat.svg
Normal file
1
src/assets/svg/chat.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1760322549363" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4833" width="200" height="200"><path d="M512 64c259.2 0 469.333333 200.576 469.333333 448s-210.133333 448-469.333333 448a484.48 484.48 0 0 1-232.725333-58.88l-116.394667 50.645333a42.666667 42.666667 0 0 1-58.517333-49.002666l29.76-125.013334C76.629333 703.402667 42.666667 611.477333 42.666667 512 42.666667 264.576 252.8 64 512 64z m0 64C287.488 128 106.666667 300.586667 106.666667 512c0 79.573333 25.557333 155.434667 72.554666 219.285333l5.525334 7.317334 18.709333 24.192-26.965333 113.237333 105.984-46.08 27.477333 15.018667C370.858667 878.229333 439.978667 896 512 896c224.512 0 405.333333-172.586667 405.333333-384S736.512 128 512 128z m-157.696 341.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z m159.018667 0a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z m158.997333 0a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z" fill="#ffffff" p-id="4834"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
src/assets/svg/inChat.svg
Normal file
1
src/assets/svg/inChat.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg t="1760324555874" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6809" width="200" height="200"><path d="M397.175467 618.461867a34.0992 34.0992 0 1 0 48.264533 48.264533l193.092267-193.058133a34.167467 34.167467 0 0 0-0.034134-48.3328L445.44 232.277333a34.0992 34.0992 0 1 0-48.264533 48.264534l134.826666 134.826666H68.266667a34.133333 34.133333 0 0 0 0 68.266667h463.735466l-134.826666 134.826667z" fill="#409eff" p-id="6810"></path><path d="M846.370133 148.0704a259.3792 259.3792 0 0 0-36.7616-29.422933C725.128533 63.556267 618.564267 34.133333 515.6864 34.133333c-29.3888 0-58.7776 3.6864-88.200533 7.338667-88.200533 11.025067-172.714667 44.1344-242.517334 99.259733a402.090667 402.090667 0 0 0-110.114133 138.103467h80.042667a323.754667 323.754667 0 0 1 70.485333-79.291733c55.125333-44.100267 124.928-73.5232 209.442133-84.548267 29.3888-7.338667 55.125333-7.338667 80.827734-7.338667 95.5392 0 183.739733 25.736533 253.5424 73.5232 11.025067 7.338667 22.050133 14.711467 29.3888 25.736534C875.793067 273.066667 919.893333 368.64 916.206933 460.526933s-55.125333 183.808-135.953066 246.272c-44.100267 33.0752-95.5392 55.125333-150.664534 69.8368-29.3888 7.338667-58.7776 11.025067-88.200533 11.025067h-58.7776c-22.050133 0-36.7616 3.6864-55.125333 7.338667-22.050133 7.338667-36.7616 14.711467-51.438934 22.050133L262.144 886.954667l11.025067-88.234667c0-11.025067 3.6864-29.422933-3.6864-51.473067-11.025067-33.0752-33.0752-51.473067-47.786667-62.498133-24.4736-17.134933-45.294933-39.458133-62.702933-64.580267H75.8784a357.7856 357.7856 0 0 0 105.403733 123.392c7.338667 7.3728 18.363733 14.711467 22.050134 25.736534 3.6864 7.338667 0 14.711467 0 22.050133-3.6864 44.100267-11.025067 88.234667-14.711467 132.334933-3.652267 25.736533-14.677333 66.184533 29.422933 66.184534 14.711467 0 29.3888-7.338667 40.413867-18.363734 51.438933-33.0752 102.877867-62.498133 154.3168-95.573333 11.025067-7.338667 22.050133-11.025067 33.0752-14.711467 11.025067-3.6864 22.050133-3.6864 36.7616-3.6864h62.464c33.0752-3.6864 66.1504-7.338667 99.2256-14.711466 62.464-14.711467 124.928-44.100267 180.053333-80.861867 95.5392-69.8368 161.6896-183.808 165.341867-301.431467S938.257067 225.28 846.370133 148.0704z" fill="#409eff" p-id="6811"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -7,7 +7,8 @@
|
||||
:mode="mode"
|
||||
/>
|
||||
<Editor
|
||||
style="height: 500px; overflow-y: hidden;"
|
||||
style="overflow-y: hidden;"
|
||||
:style="'height:'+height"
|
||||
v-model="valueHtml"
|
||||
:defaultConfig="editorConfig"
|
||||
:mode="mode"
|
||||
|
||||
@@ -39,8 +39,13 @@
|
||||
<el-icon><ProgramSvg /></el-icon>
|
||||
<template #title>项目管理</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/chat">
|
||||
<el-icon><ChatSvg /></el-icon>
|
||||
<template #title>聊天室</template>
|
||||
</el-menu-item>
|
||||
|
||||
<el-menu-item index="/transfers">
|
||||
<el-menu-item v-if="userStore.isAdmin" index="/transfers">
|
||||
<el-icon><Money /></el-icon>
|
||||
<template #title>转账管理</template>
|
||||
</el-menu-item>
|
||||
@@ -193,6 +198,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import ProgramSvg from '@/assets/svg/program.svg'
|
||||
import ChatSvg from '@/assets/svg/chat.svg'
|
||||
import {
|
||||
Odometer,
|
||||
User,
|
||||
|
||||
@@ -107,6 +107,15 @@ const routes = [
|
||||
title: '项目管理 - 炬融圈',
|
||||
icon: 'Project'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'chat',
|
||||
name: 'Chat',
|
||||
component: () => import('@/views/Chat.vue'),
|
||||
meta: {
|
||||
title: '聊天 - 炬融圈',
|
||||
icon: 'ChatDotRound'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -137,6 +137,8 @@ export const createRequest = (baseURL) => {
|
||||
export const apiRequest = createRequest(import.meta.env.VITE_API_BASE_URL || '/api')
|
||||
export const midRequest = createRequest(import.meta.env.VITE_UPLOAD_BASE_URL || '/mid')
|
||||
export const programRequest = createRequest(import.meta.env.VITE_UPLOAD_BASE_URL || '/program')
|
||||
export const groupRequest = createRequest(import.meta.env.VITE_UPLOAD_BASE_URL || '/group')
|
||||
export const messageRequest = createRequest(import.meta.env.VITE_UPLOAD_BASE_URL || '/message')
|
||||
|
||||
let loadingInstance = null
|
||||
let requestCount = 0
|
||||
|
||||
976
src/views/Chat.vue
Normal file
976
src/views/Chat.vue
Normal file
@@ -0,0 +1,976 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card class="page-header">
|
||||
<h2>客服聊天</h2>
|
||||
<p>客服聊天</p>
|
||||
</el-card>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<el-card class="stats-card">
|
||||
<el-row :gutter="24" class="stats-row">
|
||||
<el-col :span="8">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.totalTransfers }}</div>
|
||||
<div class="stat-label">总转账数</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon">
|
||||
<Money/>
|
||||
</el-icon>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">{{ stats.confirmedTransfers }}</div>
|
||||
<div class="stat-label">已确认</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon">
|
||||
<Check/>
|
||||
</el-icon>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-content">
|
||||
<div class="stat-number">¥{{ stats.totalAmount }}</div>
|
||||
<div class="stat-label">总欠额</div>
|
||||
</div>
|
||||
<el-icon class="stat-icon">
|
||||
<Wallet/>
|
||||
</el-icon>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 筛选和操作 -->
|
||||
<el-card class="filter-card">
|
||||
<el-row :gutter="20">
|
||||
<!-- <el-col :span="4">-->
|
||||
<!-- <el-select v-model="queryParams.status" placeholder="选择状态" clearable>-->
|
||||
<!-- <el-option label="全部" value=""/>-->
|
||||
<!-- <el-option label="待确认" value="pending"/>-->
|
||||
<!-- <el-option label="已确认" value="confirmed"/>-->
|
||||
<!-- <el-option label="已拒绝" value="rejected"/>-->
|
||||
<!-- <el-option label="已取消" value="cancelled"/>-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-col>-->
|
||||
|
||||
<el-col :span="4">
|
||||
<el-input v-model="queryParams.keyword" placeholder="搜索项目名称或者项目公司" clearable/>
|
||||
</el-col>
|
||||
|
||||
<!-- <el-col :span="4">-->
|
||||
<!-- <el-date-picker-->
|
||||
<!-- v-model="dateRange"-->
|
||||
<!-- type="daterange"-->
|
||||
<!-- range-separator="至"-->
|
||||
<!-- start-placeholder="开始日期"-->
|
||||
<!-- end-placeholder="结束日期"-->
|
||||
<!-- format="YYYY-MM-DD"-->
|
||||
<!-- value-format="YYYY-MM-DD"-->
|
||||
<!-- />-->
|
||||
<!-- </el-col>-->
|
||||
|
||||
<el-row style="margin-left: 50px;" :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" @click="getList">搜索</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-row>
|
||||
|
||||
</el-card>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<el-card class="table-card">
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<!-- <el-col :span="1.5">-->
|
||||
<!-- <el-button-->
|
||||
<!-- type="primary"-->
|
||||
<!-- plain-->
|
||||
<!-- icon="Plus"-->
|
||||
<!-- @click="handleAdd"-->
|
||||
<!-- >新增-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </el-col>-->
|
||||
<!-- <el-col :span="1.5">-->
|
||||
<!-- <el-button-->
|
||||
<!-- type="success"-->
|
||||
<!-- plain-->
|
||||
<!-- icon="Edit"-->
|
||||
<!-- :disabled="single"-->
|
||||
<!-- @click="handleUpdate"-->
|
||||
<!-- >修改-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </el-col>-->
|
||||
<!-- <el-col :span="1.5">-->
|
||||
<!-- <el-button-->
|
||||
<!-- type="danger"-->
|
||||
<!-- plain-->
|
||||
<!-- icon="Delete"-->
|
||||
<!-- :disabled="single"-->
|
||||
<!-- @click="handleDelete"-->
|
||||
<!-- >删除-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </el-col>-->
|
||||
</el-row>
|
||||
|
||||
<el-table
|
||||
:data="dataList"
|
||||
v-loading="loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center"/>
|
||||
<!-- <el-table-column prop="id" label="ID"/>-->
|
||||
<el-table-column prop="name" label="项目名称">
|
||||
<template #default="scope">
|
||||
{{ scope.row.program.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="company" label="项目公司">
|
||||
<template #default="scope">
|
||||
{{ scope.row.program.company }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="项目负责人">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.chargeUser.username }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="咨询人">
|
||||
<template #default="scope">
|
||||
{{ scope.row.user.username }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="消息未读数">
|
||||
<template #default="scope">
|
||||
<el-tag type="danger">{{ scope.row.customerUnread.length }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间"/>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="300">
|
||||
<template #default="scope">
|
||||
<!-- <el-button link type="primary" icon="Document" @click="handleDetail(scope.row)">详情</el-button>-->
|
||||
<!-- <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>-->
|
||||
<el-button link type="primary" :icon="inChatSvg" @click="handleChat(scope.row)">进入聊天</el-button>
|
||||
<!-- <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>-->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10,50,100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="getList"
|
||||
@current-change="getList"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 表单 -->
|
||||
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="项目名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入项目名称"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="公司名称" prop="company">
|
||||
<el-input v-model="form.company" placeholder="请输入公司名称"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目地点" prop="address">
|
||||
<el-input v-model="form.address" placeholder="请输入项目地点"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算方式" prop="paymentMethod">
|
||||
<el-input v-model="form.paymentMethod" placeholder="请输入结算方式"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="项目时间" prop="dataRange">
|
||||
<el-date-picker
|
||||
v-model="form.dataRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="开始时间" prop="startDate">-->
|
||||
<!-- <el-input v-model="form.startDate" placeholder="请选择开始时间"/>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="结束时间" prop="endDate">-->
|
||||
<!-- <el-input v-model="form.endDate" placeholder="请选择结束时间"/>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="项目描述" prop="introduction">
|
||||
<el-input v-model="form.introduction" placeholder="请输入项目简介"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 聊天室 -->
|
||||
<el-dialog
|
||||
v-model="chatDialog"
|
||||
title="客服聊天室"
|
||||
width="70%"
|
||||
class="chat-dialog"
|
||||
:before-close="closeChatDialog"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="chat-content">
|
||||
<div class="chat-messages" ref="messagesContainer">
|
||||
<div style="text-align: center">
|
||||
<el-button v-if="canLoad" @click="loadData">加载更多</el-button>
|
||||
</div>
|
||||
<div
|
||||
v-for="(message, index) in messageList"
|
||||
:key="index"
|
||||
class="message"
|
||||
:class="{
|
||||
'manager': messageChargeId === message.createId,
|
||||
'consultant': messageUserId === message.createId,
|
||||
'service': userStore.user.id === message.createId,
|
||||
}"
|
||||
>
|
||||
<div class="message-avatar">
|
||||
{{ getRoleInitial(message.createId) }}
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-sender">{{ message.userInfo.username }}</div>
|
||||
<div v-if="message.type==='text'">{{ message.content }}</div>
|
||||
<div v-if="message.type==='img'">
|
||||
<el-image class="message-img" :src="message.content" fit="contain"
|
||||
:preview-src-list="[message.content]"/>
|
||||
</div>
|
||||
<div v-if="message.type==='mp3'">
|
||||
<audio ref="audio" :src="message.content" controls="controls"></audio>
|
||||
</div>
|
||||
<div class="message-time">{{ message.createTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-input-area">
|
||||
<el-input
|
||||
v-model="inputText"
|
||||
class="chat-input"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入消息..."
|
||||
></el-input>
|
||||
<el-button type="primary" @click="handleSendMsg">发送</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {getCurrentInstance, nextTick, onMounted, reactive, ref, toRefs} from "vue";
|
||||
import {Check, Money, Wallet} from "@element-plus/icons-vue";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import {programAPI} from "@/api/program";
|
||||
import {useUserStore} from "@/stores/user";
|
||||
import {groupAPI} from "@/api/group";
|
||||
import inChatSvg from '@/assets/svg/inChat.svg'
|
||||
import {messageAPI} from "@/api/message";
|
||||
import {io} from 'socket.io-client';
|
||||
|
||||
|
||||
const {proxy} = getCurrentInstance();
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 统计
|
||||
const stats = ref({
|
||||
totalTransfers: 10,
|
||||
confirmedTransfers: 10,
|
||||
totalAmount: 10
|
||||
})
|
||||
const dateRange = ref([])
|
||||
// 列表
|
||||
const dataList = ref([])
|
||||
const loading = ref(false)
|
||||
const single = ref(true);
|
||||
// 分页
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
});
|
||||
// 表单
|
||||
const open = ref(false)
|
||||
const title = ref("");
|
||||
const data = reactive({
|
||||
form: {},
|
||||
queryParams: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
customerId: userStore.user.id,
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
keyword: undefined
|
||||
},
|
||||
rules: {
|
||||
name: [{required: true, message: "项目名称不能为空", trigger: "blur"}],
|
||||
company: [{required: true, message: "公司不能为空", trigger: "blur"}],
|
||||
address: [{required: true, message: "地址不能为空", trigger: "blur"}],
|
||||
introduction: [{required: true, message: "简介不能为空", trigger: "blur"}],
|
||||
paymentMethod: [{required: true, message: "结算方式不能为空", trigger: "blur"}],
|
||||
dataRange: [{required: true, message: "时间不能为空", trigger: "blur"}],
|
||||
},
|
||||
});
|
||||
|
||||
const {queryParams, form, rules} = toRefs(data);
|
||||
|
||||
// TODO
|
||||
const getStats = () => {
|
||||
// programAPI.getStats().then(res => {
|
||||
// stats.value = res.data.data.stats
|
||||
// })
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
// api请求
|
||||
queryParams.value.page = pagination.value.page;
|
||||
queryParams.value.size = pagination.value.size;
|
||||
groupAPI.list(queryParams.value).then(res => {
|
||||
dataList.value = res.data.data.list
|
||||
pagination.value.total = res.data.data.total
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
// 重置
|
||||
const resetFilters = () => {
|
||||
queryParams.value = {
|
||||
page: 1,
|
||||
keyword: undefined
|
||||
}
|
||||
dateRange.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleSelectionChange = (selection) => {
|
||||
single.value = selection.length != 1;
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
title.value = "新增项目"
|
||||
open.value = true;
|
||||
form.value = {}
|
||||
}
|
||||
|
||||
const handleUpdate = (row) => {
|
||||
programAPI.getOne(row.id).then(res => {
|
||||
form.value = res.data.data
|
||||
form.value.dataRange = [form.value.startDate, form.value.endDate]
|
||||
title.value = "修改项目"
|
||||
open.value = true;
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
programAPI.delete(row.id).then(res => {
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
})
|
||||
}).catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleDetail = (row) => {
|
||||
ElMessage.info('查看详情')
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
proxy.$refs["formRef"].validate(valid => {
|
||||
if (valid) {
|
||||
form.value.linkmanId = userStore.user.id
|
||||
form.value.startDate = form.value.dataRange[0]
|
||||
form.value.endDate = form.value.dataRange[1]
|
||||
if (form.value.id != undefined) {
|
||||
delete form.value.user;
|
||||
delete form.value.dataRange;
|
||||
programAPI.update(form.value).then(res => {
|
||||
ElMessage.success('修改成功')
|
||||
open.value = false
|
||||
getList()
|
||||
})
|
||||
} else {
|
||||
programAPI.add(form.value).then(res => {
|
||||
ElMessage.success('添加成功')
|
||||
open.value = false
|
||||
getList()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const cancel = () => {
|
||||
open.value = false
|
||||
}
|
||||
|
||||
// 聊天
|
||||
const chatDialog = ref(false)
|
||||
const messagesContainer = ref(null)
|
||||
const messageList = ref([])
|
||||
const messageUserId = ref(null)
|
||||
const messageChargeId = ref(null)
|
||||
const params = ref({
|
||||
groupId: undefined,
|
||||
page: 1,
|
||||
size: 10
|
||||
})
|
||||
const maxPage = ref(1)
|
||||
const canLoad = ref(true)
|
||||
const connected = ref(false)
|
||||
const socket = ref(null)
|
||||
const inputText = ref('')
|
||||
|
||||
// 打开聊天框
|
||||
const handleChat = async (row) => {
|
||||
// 加入连接
|
||||
joinRoom(row)
|
||||
setTimeout(() => {
|
||||
if (!connected.value) {
|
||||
ElMessage.error('连接失败')
|
||||
return
|
||||
}
|
||||
// 加载聊天记录
|
||||
messageUserId.value = row.userId
|
||||
messageChargeId.value = row.chargeId
|
||||
params.value.groupId = row.groupId
|
||||
loadData()
|
||||
// 未读变已读
|
||||
messageAPI.read({
|
||||
groupId: row.groupId, userId: userStore.user.id
|
||||
})
|
||||
chatDialog.value = true;
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 500)
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const joinRoom = (row) => {
|
||||
socket.value = io(`http://192.168.0.15:3007?userId=${userStore.user.id}`, {
|
||||
transports: ['websocket', 'polling'],
|
||||
timeout: 5000,
|
||||
})
|
||||
socket.value.on("connect", () => {
|
||||
console.log("连接成功")
|
||||
connected.value = true;
|
||||
socket.value.emit('addGroup', {
|
||||
groupId: row.groupId
|
||||
})
|
||||
socket.value.on('serverMsg', async (message) => {
|
||||
console.log(message)
|
||||
messageList.value.push(message)
|
||||
setTimeout(() => {
|
||||
scrollToBottom()
|
||||
}, 100)
|
||||
})
|
||||
});
|
||||
socket.value.on("leaveGroup", () => {
|
||||
console.log("leaveGroup")
|
||||
connected.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
await messageAPI.list(params.value).then(res => {
|
||||
for (let i = 0; i < res.data.data.list.length; i++) {
|
||||
messageList.value.unshift(res.data.data.list[i])
|
||||
}
|
||||
maxPage.value = res.data.data.pages
|
||||
params.value.page++
|
||||
if (params.value.page > maxPage.value) canLoad.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
if (messagesContainer.value) {
|
||||
let scrollElem = messagesContainer.value;
|
||||
scrollElem.scrollTo({top: scrollElem.scrollHeight, behavior: 'instant'});
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭聊天
|
||||
const closeChatDialog = () => {
|
||||
canLoad.value = true
|
||||
params.value.page = 1
|
||||
messageList.value = []
|
||||
|
||||
console.log("disConnection");
|
||||
socket.value.emit('leaveGroup', {
|
||||
groupId: params.value.groupId
|
||||
})
|
||||
chatDialog.value = false
|
||||
|
||||
getList()
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const handleSendMsg = () => {
|
||||
socket.value.emit('clientMsg', {
|
||||
groupId: params.value.groupId,
|
||||
createId: userStore.user.id,
|
||||
content: inputText.value,
|
||||
type: 'text'
|
||||
})
|
||||
inputText.value = ''
|
||||
}
|
||||
|
||||
// 头像字
|
||||
const getRoleInitial = (createId) => {
|
||||
if (messageUserId.value === createId) {
|
||||
return '咨'
|
||||
}
|
||||
if (messageChargeId.value === createId) {
|
||||
return '项'
|
||||
}
|
||||
if (userStore.user.id === createId) {
|
||||
return '客'
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 获取统计
|
||||
getStats()
|
||||
// 获取列表
|
||||
getList();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 40px;
|
||||
color: #e4e7ed;
|
||||
}
|
||||
|
||||
.stat-card.overdue {
|
||||
border-left: 4px solid #e6a23c;
|
||||
}
|
||||
|
||||
.stat-card.overdue .stat-icon {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.stat-card.overdue .stat-number {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-weight: bold;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.amount-red {
|
||||
font-weight: bold;
|
||||
color: #ff4949;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.proof-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.proof-image {
|
||||
width: 60%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.proof-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.proof-section h4 {
|
||||
margin-bottom: 10px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.detail-buttom {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.detail-buttom:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pay-detail-text {
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #409eff;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.user-balance-info {
|
||||
margin-top: 5px;
|
||||
padding: 5px 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #409eff;
|
||||
}
|
||||
|
||||
.real-name {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// 定义变量
|
||||
$primary-color: #409EFF;
|
||||
$success-color: #67C23A;
|
||||
$warning-color: #E6A23C;
|
||||
$info-color: #909399;
|
||||
$background-color: #f5f7fa;
|
||||
$border-color: #ebeef5;
|
||||
$text-color: #303133;
|
||||
$text-light: #606266;
|
||||
$shadow-light: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
$shadow-medium: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
|
||||
// 定义混合
|
||||
@mixin flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@mixin message-bubble($bg-color, $text-color: $text-color) {
|
||||
background-color: $bg-color;
|
||||
color: $text-color;
|
||||
box-shadow: $shadow-light;
|
||||
padding: 12px 15px;
|
||||
border-radius: 18px;
|
||||
max-width: 70%;
|
||||
position: relative;
|
||||
|
||||
.message-sender {
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: rgba($text-light, 0.7);
|
||||
margin-top: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: $background-color;
|
||||
padding: 20px;
|
||||
@include flex-center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chat-dialog {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-medium;
|
||||
|
||||
.el-dialog__header {
|
||||
background: linear-gradient(135deg, $primary-color 0%, $success-color 100%);
|
||||
margin: 0;
|
||||
|
||||
.el-dialog__title {
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 15px;
|
||||
|
||||
.el-dialog__close {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 700px;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
background-color: #f9f9f9;
|
||||
|
||||
.message {
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
&.manager {
|
||||
justify-content: flex-start;
|
||||
|
||||
.message-avatar {
|
||||
background-color: $warning-color;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
@include message-bubble(#fff);
|
||||
border-top-left-radius: 4px;
|
||||
|
||||
.message-sender {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.consultant {
|
||||
justify-content: flex-start;
|
||||
|
||||
.message-avatar {
|
||||
background-color: $primary-color;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
@include message-bubble($primary-color, white);
|
||||
border-top-right-radius: 4px;
|
||||
|
||||
.message-sender {
|
||||
color: rgba(white, 0.8);
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.service {
|
||||
justify-content: flex-end;
|
||||
|
||||
.message-avatar {
|
||||
background-color: $success-color;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
@include message-bubble(#f0f9eb);
|
||||
border-top-left-radius: 4px;
|
||||
|
||||
.message-sender {
|
||||
color: $success-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
@include flex-center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin: 0 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
.message-img {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input-area {
|
||||
padding: 15px;
|
||||
background-color: white;
|
||||
border-top: 1px solid $border-color;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.chat-dialog {
|
||||
width: 90% !important;
|
||||
|
||||
.chat-content {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 85% !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
.message {
|
||||
animation: fadeInUp 0.3s ease-out;
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba($primary-color, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -166,7 +166,7 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 表单 -->
|
||||
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
|
||||
<el-dialog :title="title" v-model="open" width="800px" append-to-body>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="项目名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入项目名称"/>
|
||||
@@ -195,8 +195,8 @@
|
||||
<!-- <el-form-item label="结束时间" prop="endDate">-->
|
||||
<!-- <el-input v-model="form.endDate" placeholder="请选择结束时间"/>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="项目描述" prop="introduction">
|
||||
<el-input v-model="form.introduction" placeholder="请输入项目简介"/>
|
||||
<el-form-item label="项目描述" prop="introduction" class="editor">
|
||||
<RichTextEditor height="300px" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -210,14 +210,18 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import {getCurrentInstance, reactive, ref, toRefs} from "vue";
|
||||
<script setup lang="ts">
|
||||
import {getCurrentInstance, reactive, ref, shallowRef, toRefs} from "vue";
|
||||
import {Check, Money, Wallet} from "@element-plus/icons-vue";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import {programAPI} from "@/api/program";
|
||||
import {useUserStore} from "@/stores/user";
|
||||
|
||||
import '@wangeditor/editor/dist/css/style.css' // 引入 css
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import {IToolbarConfig} from "@wangeditor/editor";
|
||||
import RichTextEditor from "@/components/RichTextEditor.vue";
|
||||
|
||||
const {proxy} = getCurrentInstance();
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -259,7 +263,6 @@ const data = reactive({
|
||||
dataRange: [{required: true, message: "时间不能为空", trigger: "blur"}],
|
||||
},
|
||||
});
|
||||
|
||||
const {queryParams, form, rules} = toRefs(data);
|
||||
|
||||
// TODO
|
||||
@@ -523,4 +526,10 @@ getList();
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
//::v-deep .editor{
|
||||
// .el-form-item__content{
|
||||
// border: 1px solid #000;
|
||||
// }
|
||||
//}
|
||||
</style>
|
||||
@@ -47,33 +47,33 @@
|
||||
<!-- 筛选和操作 -->
|
||||
<el-card class="filter-card">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="4">
|
||||
<el-select v-model="queryParams.status" placeholder="选择状态" clearable>
|
||||
<el-option label="全部" value=""/>
|
||||
<el-option label="待确认" value="pending"/>
|
||||
<el-option label="已确认" value="confirmed"/>
|
||||
<el-option label="已拒绝" value="rejected"/>
|
||||
<el-option label="已取消" value="cancelled"/>
|
||||
</el-select>
|
||||
</el-col>
|
||||
<!-- <el-col :span="4">-->
|
||||
<!-- <el-select v-model="queryParams.status" placeholder="选择状态" clearable>-->
|
||||
<!-- <el-option label="全部" value=""/>-->
|
||||
<!-- <el-option label="待确认" value="pending"/>-->
|
||||
<!-- <el-option label="已确认" value="confirmed"/>-->
|
||||
<!-- <el-option label="已拒绝" value="rejected"/>-->
|
||||
<!-- <el-option label="已取消" value="cancelled"/>-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-col>-->
|
||||
|
||||
<el-col :span="4">
|
||||
<el-input v-model="queryParams.search" placeholder="搜索用户名或姓名" clearable/>
|
||||
<el-input v-model="queryParams.keyword" placeholder="搜索项目名称、项目公司或地址" clearable/>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="4">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-col>
|
||||
<!-- <el-col :span="4">-->
|
||||
<!-- <el-date-picker-->
|
||||
<!-- v-model="dateRange"-->
|
||||
<!-- type="daterange"-->
|
||||
<!-- range-separator="至"-->
|
||||
<!-- start-placeholder="开始日期"-->
|
||||
<!-- end-placeholder="结束日期"-->
|
||||
<!-- format="YYYY-MM-DD"-->
|
||||
<!-- value-format="YYYY-MM-DD"-->
|
||||
<!-- />-->
|
||||
<!-- </el-col>-->
|
||||
|
||||
<el-row style="margin-left: 150px;" :gutter="10">
|
||||
<el-row style="margin-left: 50px;" :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" @click="getList">搜索</el-button>
|
||||
</el-col>
|
||||
@@ -112,7 +112,7 @@
|
||||
type="danger"
|
||||
plain
|
||||
icon="Delete"
|
||||
:disabled="multiple"
|
||||
:disabled="single"
|
||||
@click="handleDelete"
|
||||
>删除
|
||||
</el-button>
|
||||
@@ -122,12 +122,14 @@
|
||||
<el-table
|
||||
:data="dataList"
|
||||
v-loading="loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center"/>
|
||||
<el-table-column prop="id" label="ID"/>
|
||||
<!-- <el-table-column prop="id" label="ID"/>-->
|
||||
<el-table-column prop="name" label="项目名称"/>
|
||||
<el-table-column prop="company" label="项目公司"/>
|
||||
<el-table-column label="项目负责人">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ scope.row.user.username }}</el-tag>
|
||||
@@ -142,7 +144,7 @@
|
||||
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="300">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" icon="Document" @click="handleDetail(scope.row)">详情</el-button>
|
||||
<!-- <el-button link type="primary" icon="Document" @click="handleDetail(scope.row)">详情</el-button>-->
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)">修改</el-button>
|
||||
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
@@ -165,7 +167,7 @@
|
||||
|
||||
<!-- 表单 -->
|
||||
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
|
||||
<el-form ref="dictRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="项目名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入项目名称"/>
|
||||
</el-form-item>
|
||||
@@ -175,11 +177,26 @@
|
||||
<el-form-item label="项目地点" prop="address">
|
||||
<el-input v-model="form.address" placeholder="请输入项目地点"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="开始时间" prop="startDate">
|
||||
<el-input v-model="form.startDate" placeholder="请选择开始时间"/>
|
||||
<el-form-item label="结算方式" prop="paymentMethod">
|
||||
<el-input v-model="form.paymentMethod" placeholder="请输入结算方式"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" prop="endDate">
|
||||
<el-input v-model="form.endDate" placeholder="请选择结束时间"/>
|
||||
<el-form-item label="项目时间" prop="dataRange">
|
||||
<el-date-picker
|
||||
v-model="form.dataRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="开始时间" prop="startDate">-->
|
||||
<!-- <el-input v-model="form.startDate" placeholder="请选择开始时间"/>-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item label="结束时间" prop="endDate">-->
|
||||
<!-- <el-input v-model="form.endDate" placeholder="请选择结束时间"/>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item label="项目描述" prop="introduction">
|
||||
<el-input v-model="form.introduction" placeholder="请输入项目简介"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -195,10 +212,14 @@
|
||||
|
||||
<script setup>
|
||||
|
||||
import {reactive, ref, toRefs} from "vue";
|
||||
import {getCurrentInstance, reactive, ref, toRefs} from "vue";
|
||||
import {Check, Money, Wallet} from "@element-plus/icons-vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import {programAPI} from "@/api/program";
|
||||
import {useUserStore} from "@/stores/user";
|
||||
|
||||
const {proxy} = getCurrentInstance();
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 统计
|
||||
const stats = ref({
|
||||
@@ -211,7 +232,6 @@ const dateRange = ref([])
|
||||
const dataList = ref([])
|
||||
const loading = ref(false)
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
// 分页
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
@@ -226,19 +246,28 @@ const data = reactive({
|
||||
queryParams: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
dictName: undefined,
|
||||
dictType: undefined,
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
search: undefined
|
||||
keyword: undefined
|
||||
},
|
||||
rules: {
|
||||
dictName: [{required: true, message: "字典名称不能为空", trigger: "blur"}],
|
||||
dictType: [{required: true, message: "字典类型不能为空", trigger: "blur"}]
|
||||
name: [{required: true, message: "项目名称不能为空", trigger: "blur"}],
|
||||
company: [{required: true, message: "公司不能为空", trigger: "blur"}],
|
||||
address: [{required: true, message: "地址不能为空", trigger: "blur"}],
|
||||
introduction: [{required: true, message: "简介不能为空", trigger: "blur"}],
|
||||
paymentMethod: [{required: true, message: "结算方式不能为空", trigger: "blur"}],
|
||||
dataRange: [{required: true, message: "时间不能为空", trigger: "blur"}],
|
||||
},
|
||||
});
|
||||
|
||||
const {queryParams, form, rules} = toRefs(data);
|
||||
|
||||
// TODO
|
||||
const getStats = () => {
|
||||
// programAPI.getStats().then(res => {
|
||||
// stats.value = res.data.data.stats
|
||||
// })
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const getList = async () => {
|
||||
@@ -246,7 +275,6 @@ const getList = async () => {
|
||||
// api请求
|
||||
queryParams.value.page = pagination.value.page;
|
||||
queryParams.value.size = pagination.value.size;
|
||||
console.log(queryParams.value)
|
||||
programAPI.list(queryParams.value).then(res => {
|
||||
// console.log(res)
|
||||
dataList.value = res.data.data.list
|
||||
@@ -259,9 +287,14 @@ const getList = async () => {
|
||||
const resetFilters = () => {
|
||||
queryParams.value = {
|
||||
page: 1,
|
||||
size: 10
|
||||
keyword: undefined
|
||||
}
|
||||
dateRange.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
const handleSelectionChange = (selection) => {
|
||||
single.value = selection.length != 1;
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
@@ -271,11 +304,27 @@ const handleAdd = () => {
|
||||
}
|
||||
|
||||
const handleUpdate = (row) => {
|
||||
ElMessage.info('修改')
|
||||
programAPI.getOne(row.id).then(res => {
|
||||
form.value = res.data.data
|
||||
form.value.dataRange = [form.value.startDate, form.value.endDate]
|
||||
title.value = "修改项目"
|
||||
open.value = true;
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
ElMessage.info('删除')
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
programAPI.delete(row.id).then(res => {
|
||||
ElMessage.success('删除成功')
|
||||
getList()
|
||||
})
|
||||
}).catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
@@ -283,8 +332,39 @@ const handleDetail = (row) => {
|
||||
ElMessage.info('查看详情')
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
proxy.$refs["formRef"].validate(valid => {
|
||||
if (valid) {
|
||||
form.value.linkmanId = userStore.user.id
|
||||
form.value.startDate = form.value.dataRange[0]
|
||||
form.value.endDate = form.value.dataRange[1]
|
||||
if (form.value.id != undefined) {
|
||||
delete form.value.user;
|
||||
delete form.value.dataRange;
|
||||
programAPI.update(form.value).then(res => {
|
||||
ElMessage.success('修改成功')
|
||||
open.value = false
|
||||
getList()
|
||||
})
|
||||
} else {
|
||||
programAPI.add(form.value).then(res => {
|
||||
ElMessage.success('添加成功')
|
||||
open.value = false
|
||||
getList()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// getList();
|
||||
const cancel = () => {
|
||||
open.value = false
|
||||
}
|
||||
|
||||
// 获取统计
|
||||
getStats()
|
||||
// 获取列表
|
||||
getList();
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -32,10 +32,21 @@ export default defineConfig({
|
||||
rewrite: (path) => path.replace(/^\/mid/, '')
|
||||
},
|
||||
'/program': {
|
||||
target: 'http://192.168.0.12:3005/term',
|
||||
// target: 'http://192.168.0.12:3005/term',
|
||||
target: 'http://192.168.0.15:3006',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/program/, '')
|
||||
},
|
||||
'/group': {
|
||||
target: 'http://192.168.0.15:3007',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/group/, '')
|
||||
},
|
||||
'/message': {
|
||||
target: 'http://192.168.0.15:3007',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/message/, '')
|
||||
},
|
||||
'/uploads': {
|
||||
target: 'https://test.zrbjr.com',
|
||||
changeOrigin: true
|
||||
|
||||
Reference in New Issue
Block a user