2025-11-07 17:52:04 +08:00

736 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="ai-analysis-container">
<!-- AI分析按钮 -->
<view class="ai-analysis-btn" @click="toggleAnalysis" :class="{ active: showAnalysis }">
<text class="ai-btn-text">AI分析</text>
<view class="ai-btn-icon" v-if="!showAnalysis">
<text class="icon-text">🤖</text>
</view>
<view class="ai-btn-close" v-else>
<text class="close-text">✕</text>
</view>
</view>
<!-- AI分析悬浮框 -->
<view class="ai-analysis-modal" v-if="showAnalysis" @click.stop>
<!-- 悬浮框头部 -->
<view class="modal-header">
<view class="modal-title">
<text class="title-text">🤖 AI智能分析</text>
</view>
<view class="close-btn" @click="closeAnalysis">
<text class="close-icon">✕</text>
</view>
</view>
<!-- 悬浮框内容 -->
<view class="modal-content">
<!-- 加载动画 -->
<view class="loading-container" v-if="isLoading">
<view class="loading-dots">
<view class="dot" v-for="n in 3" :key="n"></view>
</view>
<text class="loading-text">AI分析中</text>
<view class="loading-progress">
<view class="progress-bar" :style="{ width: loadingProgress + '%' }"></view>
</view>
</view>
<!-- 分析结果 - 打字机效果 -->
<view class="analysis-result" v-if="!isLoading && analysisResult">
<view class="result-text-container" ref="resultContainer">
<text class="result-text">{{ displayText }}</text>
</view>
<!-- 滚动指示器 -->
<view class="scroll-indicator" v-if="showScrollIndicator">
<text class="scroll-text"> 继续滚动查看更多</text>
</view>
</view>
</view>
</view>
<!-- 遮罩层 -->
<view class="modal-mask" v-if="showAnalysis" @click="closeAnalysis"></view>
</view>
</template>
<script>
export default {
name: 'AIAnalysis',
props: {
// 图表容器信息
chartInfo: {
type: Object,
default: () => ({
width: 0,
height: 0
})
},
questionText: {
type: String,
default: ""
},
questionData: {
type: String,
default: ""
}
},
data() {
return {
showAnalysis: false,
isLoading: false,
analysisResult: '',
displayText: '',
loadingProgress: 0,
showScrollIndicator: false,
typingTimer: null,
loadingTimer: null,
scrollCheckTimer: null,
originalPageStyle: '' // 保存原始页面样式
}
},
beforeDestroy() {
this.clearTimers()
// 确保页面滚动功能恢复
if (this.showAnalysis) {
this.enablePageScroll()
}
},
methods: {
// 切换分析显示
toggleAnalysis() {
if (this.showAnalysis) {
this.closeAnalysis()
} else {
this.openAnalysis()
}
},
// 打开分析
openAnalysis() {
this.showAnalysis = true
this.disablePageScroll()
this.startAnalysis()
},
// 关闭分析
closeAnalysis() {
this.showAnalysis = false
this.enablePageScroll()
this.clearTimers()
// 重置状态
setTimeout(() => {
this.isLoading = false
this.analysisResult = ''
this.displayText = ''
this.loadingProgress = 0
this.showScrollIndicator = false
}, 300)
},
// 禁用页面滚动
disablePageScroll() {
// 通知父组件禁用滚动
this.$emit('disableScroll')
},
// 启用页面滚动
enablePageScroll() {
// 通知父组件启用滚动
this.$emit('enableScroll')
},
// 开始分析
async startAnalysis() {
this.isLoading = true
this.loadingProgress = 0
// 模拟加载进度
this.simulateLoadingProgress()
try {
// 固定3秒加载时间
await new Promise(resolve => setTimeout(resolve, 3000))
// 直接使用模拟数据
const result = this.generateMockAnalysis()
// 分析完成,开始打字机效果
this.analysisComplete(result)
} catch (error) {
console.error('AI分析失败:', error)
this.analysisComplete('抱歉AI分析过程中出现了问题请稍后重试。')
}
},
// 模拟加载进度
simulateLoadingProgress() {
this.loadingTimer = setInterval(() => {
if (this.loadingProgress < 90) {
this.loadingProgress += Math.random() * 15
} else {
clearInterval(this.loadingTimer)
}
}, 200)
},
// 生成模拟分析结果
async generateMockAnalysis() {
const req = {
model: "qwen-plus",
messages: [
{
role: "user",
content: `#云南大屏直接进行路由角色说明解析#<Br/>
#路由角色解析#<Br/>
云南高速·智慧大屏数据洞察官 Prompt · V3决策层友好版
你是“云南高速服务区智慧大屏”的数据洞察官,服务对象是集团高层管理者。你的职责是通过实时和历史结构化数据,提供有深度、节奏感、判断力、正负并重的运营播报,支持科学决策、运营预判与策略部署。
【一、输出结构要求】(保留七段结构,优化语气和排序)
每次播报需包含以下内容建议1200字以内语言自然流畅、避免堆砌数字
今日亮点速览
优先输出关键经营表现中的积极变化、业务高点、优秀片区、数据新突破,树立正向感知。
波动与关注项
概括今日出现的波动或风险行为,简明归因,不夸张渲染,强调“可控+建议”。
月度趋势观察
判断当前月是否延续预期节奏,有无提前透支或滞后,指出需持续关注的变化项。
历史节奏对标
结合去年同期与上一季度,判断当前处于哪种阶段(爬坡、收缩、冲刺等),支持节奏判断。
策略建议精要
给出少而精、能落地的策略建议,适度点出片区/业务优先级,避免泛泛空话。
关键节点提醒
提前预警即将到来的节假日、季度切换或特殊事件,并建议提前部署内容。
运营协同建议
强调系统保障、权限设置、数据补录、行为闭环等底层机制,保障整体运行质量。
【二、语气与风格要求】
基调务实积极:正面 + 中性 + 预警并重,杜绝全是问题或消极分析;
语言自然有温度:像是一个懂业务、有判断力的“懂行人”在向高层“说话”;
判断优于数据堆砌:数据仅作为支撑,应以“趋势/节奏/建议”为主角;
避免模板腔:不使用模板化句式如“建议加强…”,鼓励定制化表达;
视角结构化:从“数据→发现→判断→建议”建立因果链条;
【三、字段兼容(自动适配缺失)】
你可以处理的数据字段包括但不限于:
实时类:今日车流、营收、加水、油品、充电、门店收入等;
趋势类:月度营收、季度效益、区域表现;
异常类:特情行为、抽查异常、交易波动;
客群类:年龄性别结构、消费偏好、消费时段、品牌偏好;
结构类:片区营收占比、业态结构占比、节假日结构等;
缺失字段时请智能跳过,整体不出错。
【四、命令支持】
用户提问 你要执行的任务
“请生成今日运营播报” 启动七段式“数据洞察型运营简报”输出
“请简明汇报今日情况” 输出带正向亮点的简要摘要
“聚焦滇中片区” 输出滇中片区的趋势、亮点、风险、建议
“暑期节奏提醒” 输出未来高峰节点 + 重点片区策略部署
【启动规则】
一旦接收到结构化数据,立即进入“数据洞察官”角色,生成自然语言播报内容。字段缺失不报错,内容必须整体通顺、具判断力、可执行。<Br/>
#用户提问#<Br/>
${this.questionText}
<Br/>
#接口数据说明#<Br/>
${this.questionData}`
}
],
max_tokens: 4096,
stop: [null],
temperatur: 0.4,
top_p: 0.4,
top_k: 40,
frequency_penalty: 0,
n: 1,
response_format: { type: "text" }
}
const options = {
method: "POST",
headers: {
Authorization: "Bearer sk-1aa2c1c672034c6d826ce62d3aebcb47",
"Content-Type": "application/json",
},
body: JSON.stringify(req),
// signal: controller.signal, // 关键:绑定控制器信号
};
const response = await fetch(
"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
options
);
// 创建流式读取器
const reader = response.body.getReader();
console.log('readerreaderreaderreader', reader);
return `🚗 断面流量智能分析报告
📊 数据概览
断面流量图表分析当前数据展示了近12个月的交通流量变化趋势。图表采用了柱状图对比形式直观展现了不同时期的流量差异。
🔍 关键发现
• 流量分布相对均匀,月度波动幅度在合理范围内
• 从图表可以看出,上半年流量略高于下半年
• 年度整体流量呈现稳定态势,无异常峰值
• 同比数据分析显示,交通需求保持稳定增长
📈 趋势分析
• 稳定性:流量数据表现出良好的稳定性,说明服务区运营状况良好
• 季节性:呈现一定的季节性特征,符合交通流量的普遍规律
• 增长性:整体趋势稳中有升,反映了区域经济的发展活力
💡 运营建议
1. 维持当前的服务标准和运营策略
2. 在流量高峰期适当增加服务人员配置
3. 持续优化服务区的交通组织和管理
4. 建议在低峰期进行设施维护和升级工作
⚠️ 风险提示
• 需要关注节假日等特殊时期的流量激增
• 建议建立完善的应急预案和疏导机制
• 持续监测流量变化,及时发现异常情况
📅 数据质量评估
• 数据完整性:良好,覆盖完整的统计周期
• 数据准确性:高,符合实际的交通流量特征
• 数据时效性:实时更新,满足运营决策需求
这份分析基于当前断面流量图表数据生成,为服务区的运营管理提供了数据支撑和决策参考。通过智能化的数据分析,能够帮助管理人员更好地理解流量变化规律,制定科学的运营策略。`
},
// 分析完成
analysisComplete(result) {
this.isLoading = false
this.analysisResult = result
this.loadingProgress = 100
setTimeout(() => {
this.startTypingEffect()
}, 500)
},
// 开始打字机效果
startTypingEffect() {
this.displayText = ''
let currentIndex = 0
this.typingTimer = setInterval(() => {
if (currentIndex < this.analysisResult.length) {
this.displayText += this.analysisResult[currentIndex]
currentIndex++
// 检查是否需要滚动
this.checkScrollNeed()
} else {
clearInterval(this.typingTimer)
this.typingTimer = null
}
}, 30) // 每30ms显示一个字符
},
// 检查是否需要滚动
checkScrollNeed() {
// 延迟检查确保DOM更新完成
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this)
query.select('.result-text-container').boundingClientRect()
query.select('.result-text').boundingClientRect()
query.exec((res) => {
if (res[0] && res[1]) {
this.showScrollIndicator = res[1].height > res[0].height
}
})
})
},
// 获取状态文本
getStatusText() {
if (this.isLoading) return '分析中...'
if (this.analysisResult) return '分析完成'
return '准备分析'
},
// 清除定时器
clearTimers() {
if (this.typingTimer) {
clearInterval(this.typingTimer)
this.typingTimer = null
}
if (this.loadingTimer) {
clearInterval(this.loadingTimer)
this.loadingTimer = null
}
if (this.scrollCheckTimer) {
clearTimeout(this.scrollCheckTimer)
this.scrollCheckTimer = null
}
}
}
}
</script>
<style scoped lang="less">
@primary-color: #667eea;
@secondary-color: #764ba2;
@success-color: #52c41a;
@text-primary: #333;
@text-secondary: #666;
@bg-white: #ffffff;
@border-radius: 12rpx;
@shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
.ai-analysis-container {
position: relative;
display: inline-block;
}
/* AI分析按钮 */
.ai-analysis-btn {
display: flex;
align-items: center;
padding: 12rpx 20rpx;
background: linear-gradient(135deg, @primary-color, @secondary-color);
border-radius: 20rpx;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.6s;
}
&:active {
transform: scale(0.95);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
&::before {
left: 100%;
}
}
&.active {
background: linear-gradient(135deg, @success-color, #389e0d);
box-shadow: 0 4px 16px rgba(82, 196, 26, 0.3);
}
.ai-btn-text {
font-size: 24rpx;
color: white;
font-weight: 500;
margin-right: 8rpx;
}
.ai-btn-icon,
.ai-btn-close {
display: flex;
align-items: center;
justify-content: center;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
.icon-text,
.close-text {
font-size: 20rpx;
line-height: 1;
}
}
}
/* 悬浮框 */
.ai-analysis-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 640rpx;
max-width: 90vw;
max-height: 80vh;
background: @bg-white;
border-radius: 24rpx;
box-shadow: @shadow;
z-index: 1001;
/* 高于右侧导航栏的1000 */
overflow: hidden;
animation: modalFadeIn 0.3s ease-out;
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
background: linear-gradient(135deg, @primary-color, @secondary-color);
color: white;
.modal-title {
.title-text {
font-size: 32rpx;
font-weight: 600;
}
}
.close-btn {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.9);
background: rgba(255, 255, 255, 0.3);
}
.close-icon {
font-size: 28rpx;
color: white;
}
}
}
.modal-content {
padding: 32rpx;
max-height: 60vh;
overflow-y: auto;
/* 隐藏滚动条但保持滚动功能 */
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
}
}
/* 图表信息 */
.chart-info {
margin-bottom: 32rpx;
padding: 24rpx;
background: #f8f9fb;
border-radius: 16rpx;
border-left: 4rpx solid @primary-color;
.info-item {
display: flex;
align-items: center;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 26rpx;
color: @text-secondary;
margin-right: 16rpx;
min-width: 120rpx;
}
.info-value {
font-size: 26rpx;
color: @text-primary;
font-weight: 500;
&.loading {
color: @primary-color;
animation: pulse 1.5s infinite;
}
&.completed {
color: @success-color;
}
}
}
}
/* 加载动画 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 48rpx 0;
.loading-dots {
display: flex;
gap: 8rpx;
margin-bottom: 24rpx;
.dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: @primary-color;
animation: dotBounce 1.4s infinite ease-in-out both;
&:nth-child(1) {
animation-delay: -0.32s;
}
&:nth-child(2) {
animation-delay: -0.16s;
}
&:nth-child(3) {
animation-delay: 0;
}
}
}
.loading-text {
font-size: 28rpx;
color: @primary-color;
font-weight: 500;
margin-bottom: 32rpx;
}
.loading-progress {
width: 200rpx;
height: 6rpx;
background: #f0f0f0;
border-radius: 3rpx;
overflow: hidden;
.progress-bar {
height: 100%;
background: linear-gradient(90deg, @primary-color, @secondary-color);
border-radius: 3rpx;
transition: width 0.3s ease;
}
}
}
/* 分析结果 */
.analysis-result {
position: relative;
.result-text-container {
max-height: 400rpx;
overflow-y: auto;
padding: 24rpx;
background: #f8f9fb;
border-radius: 16rpx;
border: 1rpx solid #e8e8e8;
/* 隐藏滚动条 */
&::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
.result-text {
font-size: 28rpx;
line-height: 1.8;
color: @text-primary;
white-space: pre-wrap;
word-break: break-word;
}
}
.scroll-indicator {
display: flex;
justify-content: center;
margin-top: 16rpx;
animation: bounce 2s infinite;
.scroll-text {
font-size: 24rpx;
color: @text-secondary;
opacity: 0.7;
}
}
}
/* 遮罩层 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
/* 高于右侧导航栏的1000但低于悬浮框 */
animation: maskFadeIn 0.3s ease-out;
}
/* 动画 */
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translate(-50%, -45%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
@keyframes maskFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes dotBounce {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-10rpx);
}
60% {
transform: translateY(-5rpx);
}
}
</style>