736 lines
21 KiB
Vue
736 lines
21 KiB
Vue
<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> |