Browse Source

智能客服源码

dev
no99 3 months ago
parent
commit
2bf1371e6d
  1. 11
      .env.development
  2. 19
      .env.production
  3. 24
      .gitignore
  4. 3
      .vscode/extensions.json
  5. 14
      README.md
  6. 17
      index.html
  7. 1710
      package-lock.json
  8. 22
      package.json
  9. 1
      public/vite.svg
  10. 16
      src/App.vue
  11. 44
      src/api/sword.js
  12. BIN
      src/assets/img/avatar/小柒.png
  13. BIN
      src/assets/img/avatar/超级云脑按钮.png
  14. 50
      src/assets/js/useAppBridge.js
  15. 159
      src/assets/js/useProjectTracking.js
  16. 1
      src/assets/vue.svg
  17. 43
      src/components/HelloWorld.vue
  18. 10
      src/config/env.development.js
  19. 9
      src/config/env.production.js
  20. 32
      src/config/index.js
  21. 14
      src/main.js
  22. 20
      src/router/index.js
  23. 65
      src/store/userPermissionCode.js
  24. 79
      src/style.css
  25. 130
      src/utils/request.js
  26. 43
      src/utils/storage.js
  27. 444
      src/views/chat.vue
  28. 75
      vite.config.js

11
.env.development

@ -0,0 +1,11 @@
# must start with VITE_
VITE_ENV='development'
VITE_OUTPUT_DIR='dev'
# public path
VITE_PUBLIC_PATH=/
# VITE_APP_API_API = '/api'
#新数据接口
VITE_APP_API_BASE_URL="http://39.101.133.168:8828/link"
# Whether to open mock
VITE_USE_MOCK=true

19
.env.production

@ -0,0 +1,19 @@
# must start with VITE_
VITE_ENV = 'production'
VITE_OUTPUT_DIR = 'dist'
# public path
VITE_PUBLIC_PATH = /
# Whether to open mock
VITE_USE_MOCK = true
#新数据接口
VITE_APP_API_BASE_URL = https://api.homilychart.com/link
# Whether to enable gzip or brotli compression
# Optional: gzip | brotli | none
# If you need multiple forms, you can use `,` to separate
VITE_BUILD_COMPRESS = 'none'
# Whether to delete origin files when using compress, default false
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false

24
.gitignore

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
.vscode/extensions.json

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

14
README.md

@ -1,2 +1,14 @@
# znkf-vue
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
npm install 安装依赖
npm run dev 运行
npm run build 打包
npm install router 路由
npm install axios 网络请求
npm install element-plus --save element-plus组件库
npm install @element-plus/icons-vue 组件库图标

17
index.html

@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no" />
<title>智能客服</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1710
package-lock.json
File diff suppressed because it is too large
View File

22
package.json

@ -0,0 +1,22 @@
{
"name": "ics",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host --port 8080",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.9",
"element-plus": "^2.9.4",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"vite": "^6.1.0"
}
}

1
public/vite.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

16
src/App.vue

@ -0,0 +1,16 @@
<script setup>
import { ref, provide, onMounted } from 'vue'
// import { useProjectTracking } from './assets/js/useProjectTracking.js'
//
const projectRoutes = [
'/chat'
]
//
// useProjectTracking(projectRoutes)
// alert(location.href)
</script>
<template>
<router-view></router-view>
</template>
<style></style>

44
src/api/sword.js

@ -0,0 +1,44 @@
import request from '../utils/request'
// 路径
const APIurl = import.meta.env.VITE_APP_API_BASE_URL
//统计用户行为接口
export const computedUsersAPI = function (params) {
return request({
url: `${APIurl}/BrainStatistics/getStatistic`,
method: 'post',
data: new URLSearchParams(params),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}
//各个模块权限code接口
export const permissionAPI = function (params) {
return request({
url: `${APIurl}/api/brain/privilege`,
method: 'post',
data: new URLSearchParams(params),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}
//数据接口
export const dataListAPI = function (params) {
// URLSearchParams只接受全部字符串的数据
// 将传入数据转化成字符串
const StringParams = new URLSearchParams(
Object.entries(params).map(([key, value]) => [key, String(value)])
)
return request({
url: `${APIurl}/api/brain/data`,
method: 'post',
data: StringParams,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}

BIN
src/assets/img/avatar/小柒.png

After

Width: 250  |  Height: 250  |  Size: 68 KiB

BIN
src/assets/img/avatar/超级云脑按钮.png

After

Width: 160  |  Height: 160  |  Size: 30 KiB

50
src/assets/js/useAppBridge.js

@ -0,0 +1,50 @@
// 跳转 app 方法
export function useAppBridge() {
const fullClose = (n, m) => {
let result = Math.random() * (m + 1 - n) + n
while (result > m) {
result = Math.random() * (m + 1 - n) + n;
}
return Math.floor(result);
}
const packageFun = (funName, fun = () => { }, platform, data = {}) => {
const JWrandom = fullClose(10000, 99999)
data.JWrandom = JWrandom
window[funName + JWrandom] = fun
try {
const params = {
name: funName,
extra: { data }
}
switch (platform) {
case 2: // APP - 使用 Apicloud 框架开发
window.api.sendEvent(params);
break;
case 3: // APP - iOS 系统
window.webkit.messageHandlers.getData.postMessage(JSON.stringify(params));
break;
case 4: // APP - Android 系统
window.android.getData(JSON.stringify(params));
break;
case 5: // APP - 使用 UniApp 框架开发
window.uni.postMessage({
data: {
val: JSON.stringify(params)
}
});
break;
}
} catch (e) {
console.error('Error in packageFun:', e)
}
}
return {
packageFun,
fullClose
}
}

159
src/assets/js/useProjectTracking.js

@ -0,0 +1,159 @@
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import { useRouter } from 'vue-router'
import { computedUsersAPI } from '@/api/sword'
export function useProjectTracking(projectRoutes) {
const router = useRouter()
const entryTime = ref(Date.now())
const isInProject = ref(true)
const hasRecordedEntry = ref(sessionStorage.getItem('hasRecordedEntry') === 'true')
// const parentUrl = window.parent.location.href
// console.log('Link平台地址:', parentUrl)
let isPageRefreshing = false // 标志位:是否刷新页面
// 记录用户进入项目的时间
const recordEntryTime = () => {
if (hasRecordedEntry.value) {
return
}
entryTime.value = Date.now()
const date = new Date(entryTime.value)
const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1)
.toString()
.padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date
.getHours()
.toString()
.padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date
.getSeconds()
.toString()
.padStart(2, '0')}`
sessionStorage.setItem('projectEntryTime', formattedDate)
sessionStorage.setItem('hasRecordedEntry', 'true')
isInProject.value = true
hasRecordedEntry.value = true
console.log('记录首次进入时间:', formattedDate)
}
// 发送追踪数据到后端
const sendTrackingData = async () => {
if (!isInProject.value) return
const storedEntryTime = sessionStorage.getItem('projectEntryTime')
if (!storedEntryTime) {
console.warn('未找到存储的进入时间,取消发送跟踪数据')
return
}
let timestamp
try {
timestamp = new Date(storedEntryTime.replace(' ', 'T')).getTime()
if (isNaN(timestamp)) throw new Error('无效日期')
} catch (error) {
console.error('解析存储的进入时间时出错:', error)
return
}
const exitTime = Date.now()
const duration = Math.floor((exitTime - timestamp) / 1000)
const localToken = localStorage.getItem('localToken')
console.log('进入项目的时间', storedEntryTime)
console.log('停留时间', duration)
const params = {
stayTime: duration,
loginTime: storedEntryTime,
token: localToken
}
if (localToken) {
try {
const res = await computedUsersAPI(params)
console.log('跟踪数据已发送:', res)
sessionStorage.removeItem('projectEntryTime')
sessionStorage.removeItem('hasRecordedEntry')
isInProject.value = false
hasRecordedEntry.value = false
} catch (error) {
console.error('发送跟踪数据失败:', error)
}
}
}
// 页面可见性变化时触发
const handleVisibilityChange = () => {
// console.log(window.location.pathname.includes('duobaoqibing'), '路径是否包含了页面不可见触发')
// if (window.location.pathname.includes('duobaoqibing')) {
// console.log('在 searchCode.html 页面,不发送数据')
// return
// }
if (document.visibilityState === 'hidden') {
console.log('页面不可见,用户可能离开或切换标签页')
sendTrackingData()
}
}
// 页面关闭或刷新时触发
const handleBeforeUnload = (event) => {
// console.log(window.location.pathname)
// console.log(
// window.location.pathname.includes('duobaoqibing'),
// '路径是否包含了页面关闭了啦啦啦啦啦啦触发'
// )
// if (window.location.pathname.includes('duobaoqibing')) {
// console.log('在 searchCode.html 页面,不发送数据')
// return
// }
if (isPageRefreshing) {
console.log('页面刷新,不触发数据发送')
return
}
console.log('页面即将关闭或跳转')
sendTrackingData()
}
const handleRefreshDetection = () => {
isPageRefreshing = true
}
// 监听路由变化
watch(
() => router.currentRoute.value.path,
(newPath) => {
const isProjectRoute = projectRoutes.some((route) => newPath.startsWith(route))
let isProjectRouteName = projectRoutes[0]
console.log(isProjectRouteName)
// 判断是否是 searchCode.html 的访问
const isSearchCodePage = window.location.pathname.includes('duobaoqibing')
if (!isProjectRoute && !isSearchCodePage) {
console.log('离开项目路由:', newPath)
sendTrackingData()
} else if (isProjectRouteName && !hasRecordedEntry.value) {
console.log('首次进入项目路由:', newPath)
recordEntryTime()
}
}
)
// 添加事件监听
onMounted(() => {
document.addEventListener('visibilitychange', handleVisibilityChange)
window.addEventListener('beforeunload', handleBeforeUnload)
window.addEventListener('unload', handleRefreshDetection)
})
// 移除事件监听
onBeforeUnmount(() => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
window.removeEventListener('beforeunload', handleBeforeUnload)
window.removeEventListener('unload', handleRefreshDetection)
})
return {
entryTime,
isInProject,
sendTrackingData
}
}

1
src/assets/vue.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

43
src/components/HelloWorld.vue

@ -0,0 +1,43 @@
<script setup>
import { ref } from 'vue'
defineProps({
msg: String,
})
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

10
src/config/env.development.js

@ -0,0 +1,10 @@
// 本地环境配置
export default {
env: 'development',
title: '开发',
baseUrl: '', // 项目地址
// baseApi: 'http://39.101.133.168:8828/link', // 本地api请求地址,如果使用了代理,设置成'/'
baseApi: '', // 使用了代理设置成'/'
APPSECRET: 'xxx',
$cdn: 'https://imgs.solui.cn'
}

9
src/config/env.production.js

@ -0,0 +1,9 @@
// 正式
export default {
env: 'production',
title: '生产',
baseUrl: '', // 正式项目地址
baseApi: 'https://api.homilychart.com/scms/api', // 正式api请求地址
APPSECRET: 'xxx',
$cdn: 'https://imgs.solui.cn'
}

32
src/config/index.js

@ -0,0 +1,32 @@
// 定义配置对象结构
const IConfig = {
env: '', // 开发环境
title: '', // 项目title
baseUrl: '', // 项目地址
baseApi: '', // api请求地址
$cdn: '' // cdn公共资源路径
};
const envMap = {}; // 存储不同环境下的配置文件
// import.meta.globEager 批量导入指定目录下的模块-------导入所有的js文件(路径:模块内容)
const globalModules = import.meta.glob('./*.js', { eager: true });
Object.entries(globalModules).forEach(([key, value]) => {
// key.match(/\.\/env\.(\S*)\.ts/)
const name = key.replace(/\.\/env\.(.*)\.js$/, '$1'); // 解析出环境名称
envMap[name] = value; // 模块的内容
});
// 检查环境变量是否存在
if (!import.meta.env.VITE_ENV) {
console.error('VITE_ENV 环境变量未定义,请检查配置。');
}
// 根据环境引入不同配置
const config = envMap[import.meta.env.VITE_ENV] ? envMap[import.meta.env.VITE_ENV].default : null;
if (!config) {
console.error(`未找到对应 ${import.meta.env.VITE_ENV} 环境的配置文件,请检查。`);
}
console.log('根据环境引入不同配置', config);
export { config };

14
src/main.js

@ -0,0 +1,14 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(ElementPlus)
app.mount('#app')

20
src/router/index.js

@ -0,0 +1,20 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
redirect: 'chat'
},
{
path: '/chat',
name: 'chat',
component: () => import('../views/chat.vue')
},
]
// 创建路由实例
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_PUBLIC_PATH),
routes
})
// 导出
export default router

65
src/store/userPermissionCode.js

@ -0,0 +1,65 @@
import { ref, onMounted } from 'vue'
// 修正拼写错误
import { permissionAPI } from '../api/sword'
// 封装成一个普通的组合式函数
export const useUserInfo = () => {
const userRole = ref('')
const loading = ref(false)
const isReady = ref(false)
const getAppToken = ref('')
const getQueryVariable = (variable) => {
const query = window.location.search.substring(1)
console.log('query', query)
const vars = query.split('&')
console.log('vars', vars)
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split('=')
if (pair[0] === variable) {
return pair[1]
}
}
return ''
}
const fetchUserInfo = async () => {
getAppToken.value = localStorage.getItem('localToken')
? String(localStorage.getItem('localToken'))
: ''
loading.value = true
try {
const requestParams = {
...{ token: getAppToken.value || '' }
}
// 修正拼写错误
const res = await permissionAPI(requestParams)
// 更新状态,移除类型断言
userRole.value = res.data.userRole
isReady.value = true
} catch (err) {
console.error('Error fetching user data:', err)
} finally {
loading.value = false
}
}
const init = () => {
if (!isReady.value) {
fetchUserInfo()
}
}
onMounted(() => {
init()
})
return {
userRole,
loading,
isReady,
init,
fetchUserInfo,
getQueryVariable
}
}

79
src/style.css

@ -0,0 +1,79 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0;
padding: 0;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

130
src/utils/request.js

@ -0,0 +1,130 @@
import axios from 'axios'
import { ElMessageBox, ElMessage } from 'element-plus'
import { config } from '../config'
import router from '../router'
const ERROR_MESSAGES = {
badRequest: '请求错误(400)',
unauthorized: '未授权,请登录(401)',
forbidden: '拒绝访问(403)',
notFound: `请求地址出错: ${'[具体 URL 将在这里被替换]'}`,
methodNotAllowed: '请求方法未允许(405)',
requestTimeout: '请求超时(408)',
internalServerError: '服务器内部错误(500)',
notImplemented: '服务未实现(501)',
badGateway: '网络错误(502)',
serviceUnavailable: '服务不可用(503)',
gatewayTimeout: '网络超时(504)',
httpVersionNotSupported: 'HTTP 版本不受支持(505)',
defaultConnectionError: '连接错误: [原始错误消息]',
networkError: '网络异常,请检查后重试!',
serverFailure: '连接到服务器失败,请联系管理员'
}
const service = axios.create({
baseURL: '/zhinengkefu', // url = base url + request url+
timeout: 5000,
withCredentials: false // send cookies when cross-domain requests
// headers: {
// // clear cors
// 'Cache-Control': 'no-cache',
// Pragma: 'no-cache'
// }
})
const setErrorMsg = (error) => {
if (error && error.response) {
switch (error.response.status) {
case 400:
error.message = ERROR_MESSAGES.badRequest
break
case 401:
error.message = ERROR_MESSAGES.unauthorized
break
case 403:
error.message = ERROR_MESSAGES.forbidden
break
case 404:
error.message = ERROR_MESSAGES.notFound.replace(
'[具体 URL 将在这里被替换]',
error.response.config.url
)
break
case 405:
error.message = ERROR_MESSAGES.methodNotAllowed
break
case 408:
error.message = ERROR_MESSAGES.requestTimeout
break
case 500:
error.message = ERROR_MESSAGES.internalServerError
break
case 501:
error.message = ERROR_MESSAGES.notImplemented
break
case 502:
error.message = ERROR_MESSAGES.badGateway
break
case 503:
error.message = ERROR_MESSAGES.serviceUnavailable
break
case 504:
error.message = ERROR_MESSAGES.gatewayTimeout
break
case 505:
error.message = ERROR_MESSAGES.httpVersionNotSupported
break
default:
error.message = ERROR_MESSAGES.defaultConnectionError.replace(
'[原始错误消息]',
error.message
)
}
} else {
if (error.message === 'Network Error') {
error.message = ERROR_MESSAGES.networkError
} else {
error.message = ERROR_MESSAGES.serverFailure
}
}
return error.message
}
// Request interceptors
service.interceptors.request.use(
(config) => {
// 在此处添加请求头等,如添加 token
// if (store.state.token) {
// config.headers['Authorization'] = `Bearer ${store.state.token}`
// }
return config
},
(error) => {
return Promise.reject(error)
}
)
// Response interceptors
service.interceptors.response.use(
async (response) => {
// await new Promise(resolve => setTimeout(resolve, 3000)); // 修正拼写错误
// if (response.config.loadingInstance) {
// response.config.loadingInstance.close();
// }
const res = response.data
if (res.code !== 200) {
const errorMsg = res.msg || 'Unknown error' // 修正拼写错误
ElMessage.error(errorMsg)
// return Promise.reject(new Error(res.msg || 'Error'))
} else {
return response.data
}
},
(error) => {
const errorMessage = setErrorMsg(error)
ElMessage.error(errorMessage)
return Promise.reject(error)
}
)
export default service

43
src/utils/storage.js

@ -0,0 +1,43 @@
/**
* 封装操作localstorage本地存储的方法
*/
export const storage = {
// 存储
set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
// 取出数据
get(key) {
const value = localStorage.getItem(key);
if (value && value !== 'undefined' && value !== 'null') {
return JSON.parse(value);
}
return null;
},
// 删除数据
remove(key) {
localStorage.removeItem(key);
}
};
/**
* 封装操作sessionStorage本地存储的方法
*/
export const sessionStorageUtil = {
// 存储
set(key, value) {
window.sessionStorage.setItem(key, JSON.stringify(value));
},
// 取出数据
get(key) {
const value = window.sessionStorage.getItem(key);
if (value && value !== 'undefined' && value !== 'null') {
return JSON.parse(value);
}
return null;
},
// 删除数据
remove(key) {
window.sessionStorage.removeItem(key);
}
};

444
src/views/chat.vue

@ -0,0 +1,444 @@
<script setup>
// 使
import { ref, nextTick, watch,onMounted } from 'vue'
import { useAppBridge } from '../assets/js/useAppBridge'
import { useUserInfo } from '../store/userPermissionCode'
const { getQueryVariable } = useUserInfo()
const isTokenValid = ref(false)
const fnGetToken = () => {
localStorage.setItem('localToken', decodeURIComponent(String(getQueryVariable('token'))))
console.log(localStorage.getItem('localToken'));
}
setTimeout(() => {
fnGetToken()
}, 800)
//
const commonParams = {
token: localStorage.getItem('localToken'),
}
// token
const validateToken = async () => {
const token = localStorage.getItem('localToken')
if (!token) {
console.error('未找到 token,请重新登录')
return false
}
// const isValid = await validateTokenAPI(token)
// if (!isValid) {
// console.error('Token ')
// return false
// }
return true
}
// token
validateToken().then((isValid) => {
isTokenValid.value = isValid
if (!isValid) {
//
console.error('Token 验证失败,请重新登录')
}
})
// Props
const props = defineProps({
apiUrl: {
type: String,
default: 'http://localhost:5000/ask'
},
initialGreeting: {
type: String,
default: '您好!请问有什么可以帮助您?'
}
})
//
const messages = ref([
{
content: props.initialGreeting,
sender: 'bot',
timestamp: new Date()
}
])
const inputMessage = ref('')
const messageContainer = ref(null)
const isLoading = ref(false) //
//
const scrollToBottom = () => {
nextTick(() => {
if (messageContainer.value) {
messageContainer.value.scrollTop = messageContainer.value.scrollHeight
}
})
}
//
const sendMessage = async () => {
if (!isTokenValid.value) {
console.error('Token 验证失败,无法发送消息')
return
}
if (isLoading.value) return;
const content = inputMessage.value.trim()
if (!content) return
//
messages.value.push({
content,
sender: 'user',
timestamp: new Date()
})
//
inputMessage.value = ''
scrollToBottom()
//
isLoading.value = true
messages.value.push({
content: '我正在思考...',
sender: 'bot',
timestamp: new Date(),
isLoading: true
})
scrollToBottom()
try {
// API
const response = await fetch(props.apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question: content })
})
const data = await response.json()
//
messages.value = messages.value.filter(msg => !msg.isLoading)
//
messages.value.push({
content: data.answer,
sender: 'bot',
timestamp: new Date()
})
scrollToBottom()
} catch (error) {
console.error('API请求失败:', error)
//
messages.value = messages.value.filter(msg => !msg.isLoading)
messages.value.push({
content: '服务暂时不可用,请稍后再试',
sender: 'bot',
timestamp: new Date()
})
scrollToBottom()
} finally {
//
isLoading.value = false
}
}
//
const formatTime = (date) => {
return new Date(date).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})
}
//
const adjustInputHeight = () => {
const textarea = document.querySelector('.message-input')
textarea.style.height = 'auto'
textarea.style.height = `${textarea.scrollHeight}px`
}
//
watch(inputMessage, adjustInputHeight)
//
onMounted(async () => {
// token
fnGetToken()
// token
const isValid = await validateToken()
isTokenValid.value = isValid
if (!isValid) {
console.error('Token 验证失败,请重新登录')
}
})
</script>
<template>
<!-- 聊天容器 -->
<div class="chat-container">
<!-- 聊天框头部 -->
<div class="chat-header">夺宝奇兵智能客服</div>
<!-- 消息展示区域 -->
<div class="message-list" ref="messageContainer">
<div v-for="(message, index) in messages" :key="index" class="message-item" :class="[message.sender]">
<!-- 机器人头像 -->
<div v-if="message.sender === 'bot'" class="bot-avatar">
<img src="/src/assets/img/avatar/超级云脑按钮.png" alt="Bot Avatar">
</div>
<div class="message-bubble">
<div class="message-content">
<!-- 显示加载动画 -->
<span v-if="message.isLoading">
{{ message.content }}
<el-icon class="is-loading">
<Loading />
</el-icon>
</span>
<span v-else>{{ message.content }}</span>
</div>
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
</div>
<!-- 用户头像 -->
<div v-if="message.sender === 'user'" class="user-avatar">
<img src="/src/assets/img/avatar/小柒.png" alt="User Avatar">
</div>
</div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<textarea v-model="inputMessage" @keydown.enter.exact.prevent="isLoading ? null : sendMessage()"
placeholder="输入您的问题..." rows="1" class="message-input"></textarea>
<el-tooltip content="机器人正在思考" :disabled="!isLoading">
<template #content>
机器人正在思考
</template>
<button @click="sendMessage" :disabled="!isTokenValid || isLoading" class="send-button">
<!-- 使用ElementPlus的发送图标 -->
<span v-if="isLoading">
<el-icon class="is-loading">
<Loading />
</el-icon>
</span>
<span v-else class="send-button-content">
<el-icon>
<Position />
</el-icon>
<span> 发送</span>
</span>
</button>
</el-tooltip>
</div>
<!-- 未登录覆盖层 -->
<div v-if="!isTokenValid" class="overlay">
<div class="overlay-content">用户未登录</div>
</div>
</div>
</template>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: 90vh;
width: 90vw;
max-width: 800px;
margin: 0;
border: 1px solid #e0e0e0;
border-radius: 12px;
background: #f8f9fa;
overflow: hidden;
/* 新增样式,实现水平和垂直居中 */
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 聊天框头部样式 */
.chat-header {
background-color: #007bff;
color: white;
padding: 16px;
font-size: 1.2rem;
text-align: center;
}
.message-list {
flex: 1;
padding: 20px;
overflow-y: auto;
background: white;
}
.message-item {
display: flex;
margin-bottom: 16px;
}
.message-item.user {
justify-content: flex-end;
}
.bot-avatar {
margin-right: 10px;
}
.bot-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.user-avatar {
margin-left: 10px;
}
.user-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.message-bubble {
max-width: 80%;
padding: 12px 16px;
border-radius: 15px;
position: relative;
}
.message-content span {
display: block;
/* 确保元素显示 */
}
.message-item.user .message-bubble {
background: #007bff;
color: white;
border-bottom-right-radius: 4px;
}
.message-item.bot .message-bubble {
background: #f1f3f5;
color: #212529;
border-bottom-left-radius: 4px;
}
.message-time {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.8);
margin-top: 4px;
text-align: right;
}
.message-item.bot .message-time {
color: rgba(0, 0, 0, 0.6);
}
.input-area {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-top: 1px solid #e0e0e0;
background: white;
}
.message-input {
flex: 1;
padding: 10px 16px;
border: 1px solid #e0e0e0;
border-radius: 20px;
resize: none;
max-height: 120px;
font-family: inherit;
}
.send-button {
display: flex;
align-items: center;
justify-content: center;
width: 100px;
/* 调整宽度以适应文字 */
height: 40px;
border: none;
border-radius: 20px;
/* 调整圆角 */
background: #007bff;
color: white;
cursor: pointer;
transition: all 0.3s ease;
/* 添加过渡效果 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
/* 添加阴影 */
font-size: 16px;
/* 调整字体大小 */
font-weight: 600;
/* 调整字体粗细 */
}
.send-button:hover {
background: #0056b3;
transform: translateY(-2px);
/* 悬停时向上移动 */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
/* 悬停时增加阴影 */
}
/* 新增加载状态样式 */
.loading-state {
background: #ccc;
cursor: not-allowed;
}
.send-button-content {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
/* 调整文字和图标间距 */
}
/* .send-button {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: #007bff;
color: white;
cursor: pointer;
transition: background 0.2s;
}
.send-button:hover {
background: #0056b3;
} */
.send-button svg {
width: 20px;
height: 20px;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.8);
/* 透明度 50% 的白色背景 */
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
/* 确保覆盖层在聊天框上方 */
}
.overlay-content {
font-size: 36px;
font-weight: bold;
color: #f60707;
}
</style>

75
vite.config.js

@ -0,0 +1,75 @@
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
// 加载对应模式的环境变量
const env = loadEnv(mode, process.cwd());
let config = {
plugins: [vue()],
resolve: {
// 配置别名
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
css: {
preprocessorOptions: {
// 配置全局样式
scss: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
},
};
if (mode === 'development') {
// 开发模式下的配置
config = {
...config,
server: {
host: env.VITE_DEV_HOST || '0.0.0.0', // 允许通过网络访问,从环境变量获取host
port: parseInt(env.VITE_DEV_PORT, 10) || 8080, // 自定义端口,从环境变量获取port
open: true, // 自动打开浏览器
// 配置代理
proxy: {
'/api': {
target: env.VITE_DEV_API_URL,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
};
} else if (mode === 'production') {
// 生产模式下的配置
config = {
...config,
build: {
terserOptions: {
compress: {
drop_console: env.VITE_BUILD_DROP_CONSOLE === 'true', // 移除 console 语句,从环境变量获取是否移除console
},
},
// 配置打包输出目录
outDir: 'dist',
// 配置资源文件名格式
assetsDir: 'assets',
rollupOptions: {
output: {
// 手动分割代码
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
},
},
},
},
};
}
return config;
})
Loading…
Cancel
Save