Commit 5f3adbdc authored by 水玉婷's avatar 水玉婷
Browse files

feat:微信端项目初始化

parent 78707a69
Pipeline #31768 failed with stages
in 2 seconds
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Dependencies
node_modules/
# Build output
dist/
build/
# Environment variables
.env
.env.local
.env.development.local
.env.production.local
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Vite cache
.vite/
# Local History for Visual Studio Code
.history/
\ No newline at end of file
# ai-wechat
# 微信H5项目
基于Vue 3 + Vite构建的微信H5项目,支持静默登录和路由跳转。
## 功能特性
## Getting started
- ✅ Vue 3 + Vite 现代化开发环境
- ✅ Vue Router 路由管理
- ✅ 微信静默登录(snsapi_base)
- ✅ 响应式设计,适配移动端
- ✅ 生产环境打包优化
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
## 快速开始
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin http://10.17.65.20/gitlab/shuiyuting/ai-wechat.git
git branch -M main
git push -uf origin main
### 安装依赖
```bash
npm install
```
## Integrate with your tools
- [ ] [Set up project integrations](http://10.17.65.20/gitlab/shuiyuting/ai-wechat/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
### 开发环境
```bash
npm run dev
```
## License
For open source projects, say how it is licensed.
### 生产构建
```bash
npm run build
```
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
## 项目结构
\ No newline at end of file
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>国械小智</title>
<meta name="format-detection" content="telephone=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
\ No newline at end of file
This diff is collapsed.
{
"name": "ai-wechat",
"version": "1.0.0",
"description": "微信H5项目,支持静默登录",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.0",
"vue-router": "^4.2.0",
"@ant-design/icons-vue": "^6.1.0",
"echarts": "^6.0.0",
"ant-design-vue": "^3.2.0",
"dayjs": "^1.11.0",
"event-source-polyfill": "^1.0.31"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.0",
"vite": "^4.3.0",
"less": "^4.4.2"
}
}
\ No newline at end of file
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif;
background-color: #f5f5f5;
-webkit-font-smoothing: antialiased;
}
#app {
min-height: 100vh;
}
/* 微信浏览器样式优化 */
.wechat-browser {
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif;
}
</style>
\ No newline at end of file
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
\ No newline at end of file
import { createRouter, createWebHistory } from 'vue-router'
import wechat from '../utils/wechat'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue'),
meta: { requiresAuth: true }
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/:pathMatch(.*)*',
redirect: '/'
}
]
const router = createRouter({
history: createWebHistory('/ai/'), // 添加基础路径
routes
})
// 简化的路由守卫
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
const status = wechat.checkLoginStatus()
if (!status.isLoggedIn) {
next('/login')
return
}
}
next()
})
export default router
\ No newline at end of file
// 简化的微信静默登录工具类
class WeChatLogin {
constructor() {
this.appId = 'YOUR_WECHAT_APPID' // 替换为你的微信公众号AppID
this.isWeChat = this.checkWeChatBrowser()
this.isConfigured = this.appId !== 'YOUR_WECHAT_APPID'
}
// 检查是否为微信浏览器
checkWeChatBrowser() {
const ua = navigator.userAgent.toLowerCase()
return ua.indexOf('micromessenger') !== -1
}
// 简化的静默登录
async silentLogin() {
// 如果未配置appId,使用模拟登录
if (!this.isConfigured) {
console.log('使用模拟静默登录')
return this.mockLogin()
}
// 如果是微信浏览器,执行真实静默登录
if (this.isWeChat) {
return this.realWeChatLogin()
}
// 非微信浏览器,使用模拟登录
console.log('非微信浏览器,使用模拟登录')
return this.mockLogin()
}
// 真实微信静默登录
realWeChatLogin() {
return new Promise((resolve, reject) => {
// 检查URL中是否已有授权code
const urlParams = new URLSearchParams(window.location.search)
const code = urlParams.get('code')
if (code) {
// 已有code,直接获取用户信息
this.getUserInfo(code).then(resolve).catch(reject)
} else {
// 重定向到微信授权页面进行静默授权
this.redirectToWeChat()
}
})
}
// 重定向到微信授权页面
redirectToWeChat() {
const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname)
const scope = 'snsapi_base' // 静默授权,不弹出授权页面
const state = 'STATE_' + Date.now()
const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`
window.location.href = authUrl
}
// 获取用户信息(简化版)
async getUserInfo(code) {
try {
// 这里需要调用后端接口来获取用户信息
// 微信不允许前端直接调用获取用户信息的接口
const response = await fetch('/api/wechat/userinfo', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ code })
})
if (!response.ok) {
throw new Error('获取用户信息失败')
}
const userInfo = await response.json()
// 保存用户信息到本地存储
localStorage.setItem('wechat_user', JSON.stringify(userInfo))
return {
isLoggedIn: true,
userInfo: userInfo,
message: '微信静默登录成功'
}
} catch (error) {
console.error('获取用户信息失败:', error)
throw new Error('登录失败,请重试')
}
}
// 模拟登录(用于开发和测试)
mockLogin() {
return new Promise((resolve) => {
setTimeout(() => {
const mockUser = {
openid: 'mock_openid_' + Date.now(),
nickname: '测试用户',
headimgurl: '',
isMock: true
}
localStorage.setItem('wechat_user', JSON.stringify(mockUser))
resolve({
isLoggedIn: true,
userInfo: mockUser,
message: '模拟登录成功'
})
}, 1000) // 模拟网络延迟
})
}
// 检查登录状态
checkLoginStatus() {
const userData = localStorage.getItem('wechat_user')
if (userData) {
try {
const userInfo = JSON.parse(userData)
return {
isLoggedIn: true,
userInfo: userInfo
}
} catch (error) {
console.error('解析用户数据失败:', error)
}
}
return { isLoggedIn: false }
}
// 退出登录
logout() {
localStorage.removeItem('wechat_user')
}
// 获取当前用户信息
getCurrentUser() {
const status = this.checkLoginStatus()
return status.isLoggedIn ? status.userInfo : null
}
}
export default new WeChatLogin()
\ No newline at end of file
<template>
<AiChat
:params="chatParams"
:dialogSessionId="dialogSessionId"
:detailData="detailData"
:apiBaseUrl="apiBaseUrl"
:token="userToken"
:appCode="appCode"
customClass="chat-demo"
/>
</template>
<script setup lang="ts">
import AiChat from './components/AiChat.vue';
import { ref } from 'vue';
const apiBaseUrl = '/pedapi';
// 获取token
const userToken = '65c76a8b38f350bd1849d41d3185c3eb';
// 添加APP_CODE配置
const appCode = 'ped.pc';
const chatParams = {
appId: '83b2664019a945d0a438abe6339758d8',
stage: 'wechat-demo',
};
// const dialogSessionId = '20251028143404893-00045166';
const dialogSessionId = '';
const detailData = ref({
title: '国械小智',
});
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<div class="login-container">
<div class="login-content">
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>正在静默登录中...</p>
</div>
<div v-else-if="error" class="error">
<p class="error-text">{{ error }}</p>
<button @click="retryLogin" class="retry-btn">重试登录</button>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import wechat from '../utils/wechat'
export default {
name: 'Login',
setup() {
const router = useRouter()
const loading = ref(true)
const error = ref('')
const handleLogin = async () => {
try {
loading.value = true
error.value = ''
const result = await wechat.silentLogin()
if (result.isLoggedIn) {
// 登录成功,跳转到首页
setTimeout(() => {
router.replace('/')
}, 500)
}
} catch (err) {
console.error('登录失败:', err)
error.value = err.message || '登录失败,请重试'
loading.value = false
}
}
const retryLogin = () => {
handleLogin()
}
onMounted(() => {
// 检查是否已登录
const status = wechat.checkLoginStatus()
if (status.isLoggedIn) {
router.replace('/')
return
}
// 执行静默登录
handleLogin()
})
return {
loading,
error,
retryLogin
}
}
}
</script>
<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f5f5f5;
}
.login-content {
text-align: center;
padding: 2rem;
}
.loading p {
margin-top: 1rem;
color: #666;
font-size: 1rem;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #07c160;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
padding: 2rem;
}
.error-text {
color: #e74c3c;
margin-bottom: 1rem;
font-size: 1rem;
}
.retry-btn {
background: #07c160;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
}
.retry-btn:hover {
background: #06ae56;
}
</style>
\ No newline at end of file
This diff is collapsed.
<template>
<div class="message-chart">
<div class="chart-title">{{ title }}</div>
<div v-if="isEmpty" class="chart-empty">
<div class="empty-icon">📊</div>
<div class="empty-text">暂无数据</div>
<div class="empty-desc">当前查询条件下没有找到相关数据</div>
</div>
<div v-else ref="chartContainer" class="chart-container"></div>
<div v-if="error" class="chart-error">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
// 定义组件属性
interface Props {
chartData: any;
chartType?: any;
title?: string;
width?: number | string;
height?: number | string;
}
const props = withDefaults(defineProps<Props>(), {
chartType: { type: 'column' },
title: '数据图表',
width: '100%',
height: 'auto' // 设置为auto,完全自适应
});
const chartContainer = ref<HTMLElement>();
const chartInstance = ref<echarts.ECharts | null>(null);
const error = ref<string>('');
const isEmpty = ref<boolean>(false);
// ========== 工具函数抽离 ==========
/**
* 检查数据是否为空
*/
const checkDataEmpty = (data: any): boolean => {
if (!data) return true;
if (!data.rows || !Array.isArray(data.rows)) return true;
if (data.rows.length === 0) return true;
// 检查所有行是否都是空数据
const hasValidData = data.rows.some((row: any) => {
if (data.indexFields && Array.isArray(data.indexFields)) {
return data.indexFields.some((field: string) => {
const value = row[field];
return value !== null && value !== undefined && value !== '' && !isNaN(Number(value));
});
}
return false;
});
return !hasValidData;
};
/**
* 数字格式化工具
*/
const formatNumber = (value: any): string => {
if (value === null || value === undefined || isNaN(value) || value === '') {
return '0';
}
const numValue = Number(value);
if (isNaN(numValue)) return '0';
if (numValue === 0) return '0';
const roundedValue = Math.ceil(numValue * 100) / 100;
if (Math.abs(roundedValue) >= 100000000) {
return `${(roundedValue / 100000000).toFixed(2)}亿`;
} else if (Math.abs(roundedValue) >= 10000) {
return `${(roundedValue / 10000).toFixed(2)}万`;
} else {
return roundedValue.toFixed(2);
}
};
/**
* 获取颜色方案
*/
const getColors = () => ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1', '#fa8c16'];
/**
* 获取tooltip数据值
*/
const getTooltipValue = (param: any): any => {
if (param.data !== undefined && param.data !== null) return param.data;
if (param.value !== undefined && param.value !== null) return param.value;
return null;
};
/**
* 数据格式化工具
*/
const formatData = (data: any) => {
if (data && data.rows && Array.isArray(data.rows) && data.dimFields && data.indexFields) {
const { rows, dimFields, indexFields } = data;
if (rows.length === 0) {
return { isEmpty: true, data: [], chartType: 3 };
}
const chartConfig: any = { data: rows, chartType: 3 };
// 维度字段处理
if (dimFields.length === 1) {
chartConfig.xField = dimFields[0];
chartConfig.isGroup = false;
} else if (dimFields.length >= 2) {
chartConfig.xField = dimFields[0];
chartConfig.groupField = dimFields[1];
chartConfig.isGroup = true;
}
// 指标字段处理
if (indexFields.length === 1) {
chartConfig.yField = indexFields[0];
chartConfig.isMultiY = false;
} else if (indexFields.length >= 2) {
chartConfig.isMultiY = true;
chartConfig.yFields = indexFields;
chartConfig.xField = dimFields[0];
chartConfig.isGroup = dimFields.length >= 2;
}
return chartConfig;
} else {
throw new Error('不支持的数据格式,请使用新的结构化数据格式');
}
};
// ========== 图表配置生成器 ==========
/**
* 基础图表配置
*/
const getBaseChartConfig = () => ({
responsive: true,
animation: true,
animationDuration: 500,
animationEasing: 'cubicOut' as const,
grid: {
left: '3%',
right: '3%',
bottom: '3%',
top: '70px', // 改为固定值,为legend留出空间
containLabel: true
}
});
/**
* 基础tooltip配置
*/
const getBaseTooltipConfig = () => ({
trigger: 'item' as const,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#e8e8e8',
borderWidth: 1,
textStyle: {
color: '#333',
fontSize: 12
}
});
/**
* 基础x轴配置
*/
const getXAxisConfig = (xAxisData: string[]) => ({
type: 'category' as const,
data: xAxisData,
axisLabel: {
interval: 0,
rotate: xAxisData.length > 6 ? 45 : 0,
margin: 8,
fontSize: 12
}
});
/**
* 基础y轴配置
*/
const getYAxisConfig = (name?: string) => ({
type: 'value' as const,
...(name && { name }),
axisLabel: {
formatter: (value: number) => formatNumber(value)
},
splitLine: {
lineStyle: {
type: 'dashed' as const,
color: '#ccc',
width: 1
}
}
});
/**
* 双y轴配置
*/
const getDualYAxisConfig = (yFields: string[]) => [
{
...getYAxisConfig(yFields[0]),
position: 'left' as const,
splitLine: { lineStyle: { type: 'dashed' as const, color: '#ccc', width: 1 } }
},
{
...getYAxisConfig(yFields[1]),
position: 'right' as const,
splitLine: { lineStyle: { type: 'solid' as const, color: '#e8e8e8', width: 1 } }
}
];
/**
* 基础系列配置
*/
const getBaseSeriesConfig = (name: string, data: any[], color: string, yAxisIndex = 0) => ({
name,
type: 'bar' as const,
data,
yAxisIndex,
itemStyle: { color },
barWidth: 'auto' as const,
barGap: '30%',
barCategoryGap: '40%'
});
// ========== 图表类型配置生成器 ==========
/**
* 单指标柱状图配置生成器
*/
const createSingleColumnOption = (chartConfig: any): echarts.EChartsOption => {
const colors = getColors();
const xAxisData = [...new Set(chartConfig.data.map((item: any) => item[chartConfig.xField]))];
if (chartConfig.isGroup && chartConfig.groupField) {
const groups = [...new Set(chartConfig.data.map((item: any) => item[chartConfig.groupField]))];
const series = groups.map((group, index) => {
const groupData = chartConfig.data
.filter((item: any) => item[chartConfig.groupField] === group)
.map((item: any) => item[chartConfig.yField]);
return getBaseSeriesConfig(group, groupData, colors[index % colors.length]);
});
return {
...getBaseChartConfig(),
tooltip: {
...getBaseTooltipConfig(),
formatter: createTooltipFormatter()
},
legend: {
data: groups,
orient: 'horizontal',
left: 'center',
top: '0',
padding: [15, 20, 20, 25], // 增加四周的padding
itemGap: 15,
type: 'scroll',
textStyle: {
fontSize: 12
},
pageTextStyle: {
fontSize: 10
},
pageIconSize: 12,
pageButtonItemGap: 5
},
xAxis: getXAxisConfig(xAxisData),
yAxis: getYAxisConfig(),
series
};
} else {
const seriesData = chartConfig.data.map((item: any) => item[chartConfig.yField]);
return {
...getBaseChartConfig(),
tooltip: {
...getBaseTooltipConfig(),
formatter: createTooltipFormatter()
},
legend: { show: false },
xAxis: getXAxisConfig(xAxisData),
yAxis: getYAxisConfig(),
series: [getBaseSeriesConfig(chartConfig.yField, seriesData, colors[0])]
};
}
};
/**
* 双轴柱状图配置生成器
*/
const createDualColumnOption = (chartConfig: any): echarts.EChartsOption => {
const colors = getColors();
const xAxisData = [...new Set(chartConfig.data.map((item: any) => item[chartConfig.xField]))];
if (chartConfig.isGroup && chartConfig.groupField) {
const groups = [...new Set(chartConfig.data.map((item: any) => item[chartConfig.groupField]))];
const series1 = groups.map((group, index) => {
const groupData = chartConfig.data
.filter((item: any) => item[chartConfig.groupField] === group)
.map((item: any) => item[chartConfig.yFields[0]]);
return getBaseSeriesConfig(`${group} - ${chartConfig.yFields[0]}`, groupData, colors[index % colors.length], 0);
});
const series2 = groups.map((group, index) => {
const groupData = chartConfig.data
.filter((item: any) => item[chartConfig.groupField] === group)
.map((item: any) => item[chartConfig.yFields[1]]);
return getBaseSeriesConfig(`${group} - ${chartConfig.yFields[1]}`, groupData, colors[(index + 3) % colors.length], 1);
});
return {
...getBaseChartConfig(),
tooltip: {
...getBaseTooltipConfig(),
formatter: createTooltipFormatter()
},
legend: {
data: [...series1, ...series2].map(s => s.name),
orient: 'horizontal',
left: 'center',
top: '0',
padding: [15, 20, 20, 25], // 增加四周的padding
itemGap: 15,
type: 'scroll',
textStyle: {
fontSize: 12
},
pageTextStyle: {
fontSize: 10
},
pageIconSize: 12,
pageButtonItemGap: 5
},
xAxis: getXAxisConfig(xAxisData),
yAxis: getDualYAxisConfig(chartConfig.yFields),
series: [...series1, ...series2]
};
} else {
const series1Data = chartConfig.data.map((item: any) => item[chartConfig.yFields[0]]);
const series2Data = chartConfig.data.map((item: any) => item[chartConfig.yFields[1]]);
return {
...getBaseChartConfig(),
tooltip: {
...getBaseTooltipConfig(),
formatter: createTooltipFormatter()
},
legend: { data: [chartConfig.yFields[0], chartConfig.yFields[1]] },
xAxis: getXAxisConfig(xAxisData),
yAxis: getDualYAxisConfig(chartConfig.yFields),
series: [
getBaseSeriesConfig(chartConfig.yFields[0], series1Data, colors[0], 0),
getBaseSeriesConfig(chartConfig.yFields[1], series2Data, colors[1], 1)
]
};
}
};
/**
* 创建tooltip格式化器
*/
const createTooltipFormatter = () => {
return (params: any) => {
// 在item模式下,params是单个对象
const dataValue = getTooltipValue(params);
const seriesName = params.seriesName;
const xAxisValue = params.name || params.axisValue;
if (dataValue === null || dataValue === undefined) return '';
const formattedValue = formatNumber(dataValue);
return `${xAxisValue}<br/>${seriesName}: ${formattedValue}`;
};
};
// ========== 响应式处理逻辑 ==========
/**
* 创建响应式处理函数
*/
const createResizeHandler = (chartConfig: any, chartTypeForLogic: string) => {
let resizeTimer: NodeJS.Timeout;
return () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
if (chartInstance.value && chartContainer.value) {
const newOption = chartTypeForLogic === 'column'
? createSingleColumnOption(chartConfig)
: createDualColumnOption(chartConfig);
// 完全自适应配置
chartInstance.value.setOption({
...newOption,
animation: false
});
chartInstance.value.resize(); // ECharts会自动适应容器大小
}
}, 100);
};
};
/**
* 初始化图表大小监听
*/
const initChartResizeListener = (chartConfig: any, chartTypeForLogic: string) => {
const handleResize = createResizeHandler(chartConfig, chartTypeForLogic);
// 窗口大小变化监听
window.addEventListener('resize', handleResize);
// ResizeObserver监听
if (typeof ResizeObserver !== 'undefined' && chartContainer.value) {
const resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(chartContainer.value);
onUnmounted(() => {
resizeObserver.disconnect();
});
}
// 清理函数
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
};
// ========== 主图表创建函数 ==========
/**
* 创建ECharts图表
*/
const createChart = () => {
try {
if (!chartContainer.value) return;
// 检查数据是否为空
isEmpty.value = checkDataEmpty(props.chartData);
if (isEmpty.value) {
// 清理之前的图表实例
if (chartInstance.value) {
chartInstance.value.dispose();
chartInstance.value = null;
}
error.value = '';
return;
}
const chartConfig = formatData(props.chartData);
// 根据数据特征自动选择图表类型
const chartTypeForLogic = chartConfig.isMultiY && chartConfig.yFields && chartConfig.yFields.length > 1
? 'dualColumn'
: 'column';
// 创建ECharts实例
chartInstance.value = echarts.init(chartContainer.value);
// 生成图表配置
const option = chartTypeForLogic === 'column'
? createSingleColumnOption(chartConfig)
: createDualColumnOption(chartConfig);
// 设置图表选项
chartInstance.value.setOption(option);
error.value = '';
// 初始化响应式监听
initChartResizeListener(chartConfig, chartTypeForLogic);
} catch (err: any) {
error.value = `图表渲染失败: ${err.message}`;
console.error('图表渲染失败:', err);
}
};
// ========== 生命周期和响应式 ==========
// 监听数据变化
watch(() => props.chartData, createChart);
onMounted(() => {
nextTick(createChart);
});
// 组件卸载时清理
onUnmounted(() => {
if (chartInstance.value) {
chartInstance.value.dispose();
}
});
</script>
<style scoped>
.message-chart {
margin: 16px 0;
/* 完全自适应,不设置固定高度 */
/* 确保宽度不超过父容器 */
box-sizing: border-box; /* 包含边框和内边距在宽度计算中 */
}
.chart-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
color: #333;
width: 100%; /* 标题宽度限制 */
box-sizing: border-box;
}
.chart-container {
border: 1px solid #e8e8e8;
border-radius: 4px;
/* 正常图表保持宽高比自适应 */
aspect-ratio: 3 / 2; /* 宽高比 3:2 */
min-height: 200px; /* 最小高度确保显示效果 */
width: 100%; /* 确保宽度不超过父容器 */
box-sizing: border-box; /* 包含边框在宽度计算中 */
overflow: hidden; /* 防止内容溢出导致滚动条 */
}
/* 空状态样式 */
.chart-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 350px;
border: 1px solid #e8e8e8;
border-radius: 4px;
background-color: #fafafa;
color: #999;
width: 100%; /* 确保宽度不超过父容器 */
box-sizing: border-box; /* 包含边框在宽度计算中 */
overflow: hidden; /* 防止内容溢出 */
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #e8e8e8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.empty-text {
font-size: 16px;
font-weight: 500;
margin-bottom: 8px;
color: #666;
text-align: center; /* 文字居中,避免溢出 */
max-width: 100%; /* 文字宽度限制 */
}
.empty-desc {
font-size: 14px;
color: #999;
text-align: center; /* 文字居中,避免溢出 */
max-width: 100%; /* 文字宽度限制 */
}
</style>
\ No newline at end of file
// 全局滚动条美化 - 悬浮式不占位滚动条
* {
padding: 0;
margin: 0;
box-sizing: border-box;
// Webkit浏览器滚动条样式(Chrome, Safari, Edge)
&::-webkit-scrollbar {
width: 0px; // 垂直滚动条宽度
height: 0px; // 水平滚动条高度
}
}
p,h1,h2,h3,h4,h5,h6,ul,ol,li{
margin:0;
padding:0;
}
.chat-container {
background-color: white;
border-radius: 8px;
box-shadow: 0 8px 30px rgba(91, 138, 254, 0.15); // 修改阴影颜色
overflow: hidden;
width: 100%;
display: flex;
flex-direction: column;
height: 100vh;
// 居中介绍页面样式
.chat-intro-center {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px 20px;
.intro-content {
text-align: center;
max-width: 400px;
width: 100%;
.avatar-image {
width: 180px;
height: 180px;
border-radius: 50%;
margin-bottom: 20px;
}
h3 {
font-size: 24px;
color: #333;
margin-bottom: 12px;
font-weight: 600;
}
p {
font-size: 16px;
color: #666;
line-height: 1.5;
margin-bottom: 30px;
}
.start-chat-btn {
background: linear-gradient(135deg, #5B8AFE 0%, #7BA6FF 100%);
color: white;
border: none;
padding: 12px 30px;
border-radius: 25px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(91, 138, 254, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(91, 138, 254, 0.4);
}
&:active {
transform: translateY(0);
}
}
}
}
// 聊天头部样式保持不变
.chat-header {
display: flex;
align-items: center;
}
// 消息区域样式保持不变
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 12px;
}
// 输入容器保持在底部
.chat-input-container {
padding: 20px;
border-top: 1px solid #e8f2f1;
background: #FCFCFC;
// 确保输入容器始终在底部
flex-shrink: 0;
}
.header-avatar {
width: 45px;
height: 45px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin-right: 15px;
border: 2px solid rgba(255, 255, 255, 0.3);
img{
width:100%;
height:auto;
}
}
.header-info {
flex: 1;
h2 {
font-size: 18px;
}
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding:12px;
}
.message-time {
color: #88a5d0; // 修改时间颜色为蓝色系
margin-bottom: 6px;
font-size: 14px;
}
.message {
display: flex;
margin-bottom: 20px;
flex-direction: column;
align-items: baseline;
}
.message.sent {
align-items: flex-end;
.message-time {
text-align: right;
}
.avatar-container {
flex-direction: row-reverse;
justify-content: flex-end;
}
}
.avatar-container {
display: flex;
align-items: center;
}
.avatar {
width: 42px;
height: 42px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
background-color: #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: white;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
img{
width:100%;
height:auto;
}
}
.message.received .avatar {
margin-right: 12px;
}
.message.sent .avatar {
background: linear-gradient(135deg, #A8C6FF 0%, #C2D6FF 100%); // 修改发送方头像背景为浅蓝色
margin-left: 12px;
}
.message-content-wrapper {
max-width: 100%;
margin-top: 8px;
min-width: 150px;
// 当包含图表、表格或iframe时,宽度为100%
&:has(.message-table),
&:has(.message-chart),
&:has(.message-iframe){
width: 100%;
min-width: 100%;
max-width: 100%;
}
}
.message-content {
padding: 10px;
border-radius: 8px;
line-height: 1.5;
position: relative;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
white-space: pre-wrap;
.message-inner-box {
font-size: 0;
:deep(.message-text) {
font-size: 14px;
}
}
}
.message.received .message-content {
background-color: #f0f5ff; // 修改回复方消息背景为浅蓝色
border-top-left-radius: 5px;
color: #333;
border: 1px solid #e0e8ff; // 修改边框颜色
}
.message.sent .message-content {
background: linear-gradient(135deg, #5B8AFE 0%, #7BA6FF 100%); // 修改发送方消息背景为#5B8AFE
border-top-right-radius: 5px;
color: white; // 修改文字颜色为白色
border: 1px solid #4a7df5; // 修改边框颜色
}
:deep(.message-error) {
line-height: 1.5;
color: #c33;
white-space: pre-wrap;
font-size: 14px;
}
.think-box-wrapper {
margin: 12px 0;
padding: 10px 15px;
background-color: #f5f8f7;
border-radius: 12px;
border: 1px solid #e3ecea;
transition: all 0.3s ease;
}
:deep(.think-box-toggle) {
color: #5B8AFE; // 修改思考框切换按钮颜色
cursor: pointer;
font-size: 12px;
text-decoration: none;
align-items: center;
gap: 4px;
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s ease;
}
:deep(.think-box-content) {
margin-top: 8px;
background-color: #ffffff;
padding: 12px 16px;
border-radius: 8px;
border: 1px solid #e9ecef;
font-size: 0px;
color: #495057;
white-space: pre-wrap;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
animation: fadeIn 0.3s ease-in-out;
.think-content {
font-size: 0;
.think-line {
font-size: 13px;
color: #999;
font-style: italic;
}
}
}
.chat-input-container {
padding: 20px;
border-top: 1px solid #e8f2f1;
background: #FCFCFC;
}
.chat-input {
display: flex;
align-items: flex-end;
position: relative; // 添加相对定位
}
.chat-input textarea {
flex: 1;
padding: 14px 70px 14px 18px; // 增加右侧内边距为按钮留出更多空间
border-radius: 12px;
outline: none;
resize: none;
height: 52px;
font-size: 15px;
transition: border-color 0.3s, box-shadow 0.3s;
background-color: #f8faff;
border: 1px solid #E0E0E0;
overflow: hidden;
position: relative;
/* 投影/大投影 */
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
}
.chat-input button {
position: absolute;
right: 12px;
top: 50%;
color: #5B8AFE;
background: none;
border: none;
width: 40px;
height: 100%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: color 0.2s, border-color 0.2s, background-color 0.2s;
z-index: 10;
transform: translateY(-50%);
}
.chat-input button:hover {
color: #4a7df5; // 悬停时颜色变深
border-color: #4a7df5; // 悬停时边框颜色变深
background-color: rgba(91, 138, 254, 0.05); // 悬停时添加轻微背景色
}
.chat-input button:active {
background-color: rgba(91, 138, 254, 0.1); // 点击时背景色加深
transform: translateY(-50%) scale(0.95); // 保持垂直居中并缩小
}
.chat-input button:disabled {
color: #cccccc; // 禁用状态颜色
border-color: #cccccc; // 禁用状态边框颜色
background-color: transparent; // 禁用状态背景透明
cursor: not-allowed;
transform: translateY(-50%); // 保持垂直居中,不移除transform
}
.chat-input textarea:focus {
border-color: #5B8AFE; // 修改输入框焦点边框颜色
box-shadow: 0 0 0 2px rgba(91, 138, 254, 0.1); // 修改输入框焦点阴影
}
.chat-input textarea:disabled {
background-color: #f5f5f5;
border-color: #e0e0e0;
color: #999;
cursor: not-allowed;
}
.operation-box {
margin-top: 6px;
p {
color: #999;
font-size: 12px;
span {
margin-right: 15px;
}
}
}
}
@media (max-width: 600px) {
.header-avatar {
width: 40px;
height: 40px;
}
}
// 表格消息样式
:deep(.message-table) {
width: 100%;
max-width: 100%;
margin: 8px 0;
// 表格容器,用于包裹表格和滚动条
.table-container {
width: 100%;
overflow-x: auto; // 添加水平滚动条支持
-webkit-overflow-scrolling: touch; // iOS平滑滚动
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
// 滚动条样式
&::-webkit-scrollbar {
height: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
&:hover {
background: #a8a8a8;
}
}
}
.data-table {
width: auto; // 改为自动宽度,允许表格超出容器
min-width: 100%; // 确保表格至少与容器一样宽
border-collapse: collapse;
background-color: white;
table-layout: auto; // 改为自动布局,允许列宽自适应内容
// 文字列样式 - 左对齐
.text-cell {
text-align: left;
padding-left: 12px;
padding-right: 8px;
}
// 数字列样式 - 右对齐
.numeric-cell {
text-align: right;
padding-left: 8px;
padding-right: 12px;
font-family: 'Courier New', monospace; // 使用等宽字体便于数字对齐
font-weight: 500;
}
// 趋势列样式 - 居中
.trend-cell {
text-align: center;
padding-left: 8px;
padding-right: 8px;
}
th {
background: linear-gradient(135deg, #5B8AFE 0%, #4a7df5 100%); // 修改表格表头背景
color: white;
font-weight: 600;
padding: 12px 8px;
font-size: 14px;
border-right: 1px solid rgba(255, 255, 255, 0.2);
height: 40px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 80px;
&:last-child {
border-right: none;
}
}
td {
padding: 10px 8px;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
color: #333;
height: 35px; // 固定单元格高度,替换line-height
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle; // 确保内容垂直居中
min-width: 80px; // 设置最小列宽,与表头保持一致
&:nth-child(odd) {
background-color: #f8faff; // 修改奇数行背景色为浅蓝色
}
&:nth-child(even) {
background-color: white;
}
}
tr:hover td {
background-color: #e8f6f4;
}
tr:last-child td {
border-bottom: none;
}
}
// 趋势箭头样式
.trend-up {
color: #52c41a;
font-weight: bold;
font-size: 16px;
}
.trend-down {
color: #f5222d;
font-weight: bold;
font-size: 16px;
}
.table-footer {
margin-top: 12px;
font-size: 14px;
span {
color: #2eb0a1;
font-weight: bold;
}
}
}
// 响应式表格样式
@media (max-width: 768px) {
:deep(.message-table) {
.data-table {
font-size: 12px;
th, td {
padding: 8px 4px;
height: 30px; // 移动端减小高度
min-width: 60px; // 移动端减小最小列宽
}
}
.table-title {
font-size: 14px;
}
.table-summary {
font-size: 12px;
padding: 8px 10px;
}
}
}
@media (max-width: 480px) {
:deep(.message-table) {
.data-table {
th, td {
min-width: 50px; // 更小屏幕进一步减小最小列宽
height: 28px; // 更小屏幕进一步减小高度
}
}
}
}
// 表格消息样式
:deep(.message-iframe) {
width: 100%;
max-width: 100%;
margin: 8px 0;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
iframe {
width: 100%;
height: 100%;
min-height: 1800px;
border: none;
border-radius: 8px;
background-color: #f8f9fa;
transition: height 0.5s ease-in-out, opacity 0.3s ease;
// 加载状态样式
&[src=""] {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
}
// 加载状态
&.iframe-loading {
iframe {
opacity: 0;
pointer-events: none;
min-height: 400px;
}
.iframe-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
z-index: 10;
}
}
// 加载完成状态
&.iframe-loaded {
iframe {
opacity: 1;
}
.iframe-loading {
display: none;
}
}
// 加载动画
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #e0e0e0;
border-top: 4px solid #5B8AFE;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 16px;
}
.loading-text {
font-size: 16px;
color: #666;
margin-bottom: 12px;
font-weight: 500;
}
.loading-progress {
width: 200px;
height: 4px;
background: #e0e0e0;
border-radius: 2px;
overflow: hidden;
.progress-bar {
height: 100%;
background: linear-gradient(90deg, #5B8AFE, #7BA6FF);
width: 30%;
animation: progress 2s ease-in-out infinite;
}
}
}
// 加载动画
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes progress {
0% {
transform: translateX(-100%);
}
50% {
transform: translateX(200%);
}
100% {
transform: translateX(200%);
}
}
\ No newline at end of file
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path';
export default defineConfig({
base: '/ai/', // 添加基础路径前缀
plugins: [vue()],
server: {
host: '0.0.0.0',
port: 3000,
proxy: {
'/pedapi': {
target: 'http://peddev.cmic.com.cn',
changeOrigin: true, // 解决跨域问题
secure: false, // 允许不安全的SSL连接
configure: (proxy, options) => {
proxy.on('error', (err, req, res) => {
console.log('代理错误:', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('发送请求到:', options.target);
});
}
},
'/WeChatOauth2': {
target: 'http://peddev.cmic.com.cn',
changeOrigin: true, // 解决跨域问题
secure: false, // 允许不安全的SSL连接
configure: (proxy, options) => {
proxy.on('error', (err, req, res) => {
console.log('代理错误:', err);
});
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('发送请求到:', options.target);
});
}
}
},
},
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false
},
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'echarts': 'echarts/dist/echarts.esm.js' // 添加这一行
}
},
})
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment