28 changed files with 3054 additions and 1 deletions
			
			
		- 
					11.env.development
 - 
					19.env.production
 - 
					24.gitignore
 - 
					3.vscode/extensions.json
 - 
					14README.md
 - 
					17index.html
 - 
					1710package-lock.json
 - 
					22package.json
 - 
					1public/vite.svg
 - 
					16src/App.vue
 - 
					44src/api/sword.js
 - 
					BINsrc/assets/img/avatar/小柒.png
 - 
					BINsrc/assets/img/avatar/超级云脑按钮.png
 - 
					50src/assets/js/useAppBridge.js
 - 
					159src/assets/js/useProjectTracking.js
 - 
					1src/assets/vue.svg
 - 
					43src/components/HelloWorld.vue
 - 
					10src/config/env.development.js
 - 
					9src/config/env.production.js
 - 
					32src/config/index.js
 - 
					14src/main.js
 - 
					20src/router/index.js
 - 
					65src/store/userPermissionCode.js
 - 
					79src/style.css
 - 
					130src/utils/request.js
 - 
					43src/utils/storage.js
 - 
					444src/views/chat.vue
 - 
					75vite.config.js
 
@ -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 | 
				
			|||
@ -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 | 
				
			|||
@ -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? | 
				
			|||
@ -0,0 +1,3 @@ | 
				
			|||
{ | 
				
			|||
  "recommendations": ["Vue.volar"] | 
				
			|||
} | 
				
			|||
@ -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 组件库图标 | 
				
			|||
@ -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
								
							
						
					
				File diff suppressed because it is too large
							
							
								
									View File
								
							
						@ -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" | 
				
			|||
  } | 
				
			|||
} | 
				
			|||
@ -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> | 
				
			|||
@ -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> | 
				
			|||
@ -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' | 
				
			|||
    } | 
				
			|||
  }) | 
				
			|||
} | 
				
			|||
| 
		 After Width: 250 | Height: 250 | Size: 68 KiB  | 
| 
		 After Width: 160 | Height: 160 | Size: 30 KiB  | 
@ -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 | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
@ -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 | 
				
			|||
	} | 
				
			|||
} | 
				
			|||
@ -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> | 
				
			|||
@ -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> | 
				
			|||
@ -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' | 
				
			|||
} | 
				
			|||
@ -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' | 
				
			|||
} | 
				
			|||
@ -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 }; | 
				
			|||
@ -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') | 
				
			|||
@ -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 | 
				
			|||
@ -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 | 
				
			|||
    } | 
				
			|||
} | 
				
			|||
@ -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; | 
				
			|||
  } | 
				
			|||
} | 
				
			|||
@ -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 | 
				
			|||
@ -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); | 
				
			|||
    } | 
				
			|||
}; | 
				
			|||
@ -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> | 
				
			|||
@ -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; | 
				
			|||
}) | 
				
			|||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue