3475 lines
108 KiB
Vue
3475 lines
108 KiB
Vue
<template>
|
||
<!-- 主内容区域 - 登录时禁用交互 -->
|
||
<view class="page" v-if="!isH5" :class="{ 'page-disabled': showLoginModal }">
|
||
<view class="section">
|
||
<view class="section-title">核心功能</view>
|
||
<view class="grid">
|
||
<view class="card card-core card-warning" @tap="goWarning">
|
||
<view class="card-accent"></view>
|
||
<view class="card-icon card-icon-solid">
|
||
<uni-icons class="card-icon-inner" type="notification" size="28" color="#FFFFFF"></uni-icons>
|
||
</view>
|
||
<view class="card-title">预警中心</view>
|
||
<view class="card-desc">查看预警、处置与跟进</view>
|
||
</view>
|
||
<view class="card card-core card-profile" @tap="goProfile">
|
||
<view class="card-accent"></view>
|
||
<view class="card-icon card-icon-solid">
|
||
<uni-icons class="card-icon-inner" type="person" size="28" color="#FFFFFF"></uni-icons>
|
||
</view>
|
||
<view class="card-title">个体画像</view>
|
||
<view class="card-desc">多量表趋势与风险提示</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="section-title">分析工具</view>
|
||
<view class="grid">
|
||
<view class="card card-sub card-analysis card-purple" @tap="goSentenceCrimeAnalysis">
|
||
<view class="card-accent card-accent-purple"></view>
|
||
<view class="card-icon">
|
||
<uni-icons class="card-icon-inner" type="list" size="26" color="#6F63D9"></uni-icons>
|
||
</view>
|
||
<view class="card-title">刑期 / 罪名分析</view>
|
||
<view class="card-desc">对比曲线与阈值趋势</view>
|
||
</view>
|
||
<view class="card card-sub card-analysis card-blue" @tap="goComprehensive">
|
||
<view class="card-accent card-accent-blue"></view>
|
||
<view class="card-icon">
|
||
<uni-icons class="card-icon-inner" type="paperplane" size="26" color="#3B82F6"></uni-icons>
|
||
</view>
|
||
<view class="card-title">综合报告</view>
|
||
<view class="card-desc">报表汇总与折叠查看</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="section">
|
||
<view class="section-title">工具 & 基础功能</view>
|
||
<view class="grid">
|
||
<view class="card card-sub card-tools" @tap="goVoice">
|
||
<view class="card-accent card-accent-primary"></view>
|
||
<view class="card-icon">
|
||
<uni-icons class="card-icon-inner" type="mic" size="26" color="#1677ff"></uni-icons>
|
||
</view>
|
||
<view class="card-title">语音助手</view>
|
||
<view class="card-desc">按住说话,转写执行</view>
|
||
<view class="fold" @tap.stop="toggleVoiceTips">
|
||
<view class="fold-header">
|
||
<view class="fold-title">快捷指令示例</view>
|
||
<uni-icons :type="voiceTipsOpen ? 'top' : 'bottom'" size="14" color="#64748B"></uni-icons>
|
||
</view>
|
||
<view v-if="voiceTipsOpen" class="fold-body">
|
||
<view class="fold-item">“打开预警中心”</view>
|
||
<view class="fold-item">“查看3号监区本月数据”</view>
|
||
<view class="fold-item">“查询高风险人员名单”</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="card card-sub card-tools card-orange" @tap="goChartTemplates">
|
||
<view class="card-accent card-accent-orange"></view>
|
||
<view class="card-icon">
|
||
<uni-icons class="card-icon-inner" type="bars" size="26" color="#F59E0B"></uni-icons>
|
||
</view>
|
||
<view class="card-title">自定义图表</view>
|
||
<view class="card-desc">维度选择、模板保存与下钻</view>
|
||
</view>
|
||
|
||
<view class="card card-sub card-group card-wide" @tap="goInterventionTasks">
|
||
<view class="card-accent card-accent-slate"></view>
|
||
<view class="card-icon">
|
||
<uni-icons class="card-icon-inner" type="flag" size="26" color="#6366F1"></uni-icons>
|
||
</view>
|
||
<view class="card-title">任务 & 筛选</view>
|
||
<view class="card-desc">任务处置、标签筛人、监区概览</view>
|
||
<view class="mini-actions" @tap.stop>
|
||
<view class="mini" @tap="goInterventionTasks">
|
||
<uni-icons type="notification" size="16" color="#6366F1"></uni-icons>
|
||
<view class="mini-text">干预任务</view>
|
||
</view>
|
||
<view class="mini" @tap="goTagFilter">
|
||
<uni-icons type="compose" size="16" color="#6366F1"></uni-icons>
|
||
<view class="mini-text">标签筛选</view>
|
||
</view>
|
||
<view class="mini" @tap="goDashboard">
|
||
<uni-icons type="home" size="16" color="#6366F1"></uni-icons>
|
||
<view class="mini-text">监区看板</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-else class="page big">
|
||
<view class="big-top">
|
||
<view class="big-title"><text class="big-title-text">AI心理大数据平台</text></view>
|
||
</view>
|
||
<view class="big-top-actions">
|
||
<view class="big-top-action-row">
|
||
<view class="big-top-action-item" @tap="goWarning">
|
||
<image class="big-top-action-ico" src="/static/5.png" mode="aspectFit" />
|
||
</view>
|
||
<view class="big-top-action-item" @tap="goProfile">
|
||
<image class="big-top-action-ico" src="/static/4.png" mode="aspectFit" />
|
||
</view>
|
||
<view class="big-top-action-item" @tap="goComprehensive">
|
||
<image class="big-top-action-ico" src="/static/3.png" mode="aspectFit" />
|
||
</view>
|
||
<view class="big-top-action-item" @tap="goTagFilter">
|
||
<image class="big-top-action-ico" src="/static/1.png" mode="aspectFit" />
|
||
</view>
|
||
<view class="big-top-action-item" @tap="goInterventionTasks">
|
||
<image class="big-top-action-ico" src="/static/6.png" mode="aspectFit" />
|
||
</view>
|
||
<view class="big-top-action-item" @tap="goNotice">
|
||
<image class="big-top-action-ico" src="/static/7.png" mode="aspectFit" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="bigErrorMsg" class="big-error">{{ bigErrorMsg }}</view>
|
||
|
||
<view class="big-grid">
|
||
<view class="big-col">
|
||
<view class="big-panel">
|
||
<view class="big-panel-head">
|
||
<view class="big-panel-title">分析工具</view>
|
||
<view class="big-panel-sub">刑期 / 罪名分析</view>
|
||
</view>
|
||
<view class="big-chart big-chart-xl">
|
||
<qiun-data-charts v-if="isMounted" :key="'bigLine-' + chartKey" type="line" :opts="bigLineOpts" :chartData="bigLineData" canvasId="bigLine" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="big-panel">
|
||
<view class="big-panel-head">
|
||
<view class="big-panel-title">数据</view>
|
||
</view>
|
||
<view class="big-kpis">
|
||
<view class="big-kpi">
|
||
<view class="big-kpi-num">{{ bigKpi.totalAssessments }}</view>
|
||
<view class="big-kpi-lab">测评总数</view>
|
||
</view>
|
||
<view class="big-kpi">
|
||
<view class="big-kpi-num-row">
|
||
<view class="big-kpi-num">{{ bigKpi.warningReports }}</view>
|
||
<image class="big-kpi-ico" src="/static/yujing.png" mode="aspectFit" />
|
||
</view>
|
||
<view class="big-kpi-lab">预警报告</view>
|
||
</view>
|
||
<view class="big-kpi">
|
||
<view class="big-kpi-num">{{ bigKpi.tasks }}</view>
|
||
<view class="big-kpi-lab">干预任务</view>
|
||
</view>
|
||
<view class="big-kpi">
|
||
<view class="big-kpi-num">{{ bigKpi.pending }}</view>
|
||
<view class="big-kpi-lab">待处理</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="big-panel big-panel-core">
|
||
<view class="big-panel-head">
|
||
<view class="big-panel-title">核心功能</view>
|
||
</view>
|
||
<view class="big-metrics">
|
||
<view class="big-metric">
|
||
<view class="big-metric-label">各监区量表测试总数</view>
|
||
<view class="big-metric-value">{{ formatBigNumber(bigDeptAssessTotal) }}</view>
|
||
</view>
|
||
</view>
|
||
<view class="big-ring">
|
||
<qiun-data-charts v-if="isMounted" :key="'bigRing-' + chartKey" type="ring" :opts="bigRingOpts" :chartData="bigRingData" canvasId="bigRing" />
|
||
<view class="big-ring-center" @tap="goWarning">
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="big-center">
|
||
<view class="big-center-media">
|
||
<video
|
||
v-if="centerVideoUrl"
|
||
class="big-center-video"
|
||
:src="centerVideoUrl"
|
||
controls
|
||
autoplay
|
||
loop
|
||
muted
|
||
playsinline
|
||
show-mute-btn
|
||
></video>
|
||
<view v-else class="big-center-bg" aria-hidden="true"></view>
|
||
</view>
|
||
<view class="big-center-text">
|
||
<view class="big-center-text-title">{{ bigCenterTextTitle }}</view>
|
||
<view class="big-center-text-desc">{{ bigCenterTextDesc }}</view>
|
||
</view>
|
||
|
||
<view :class="['big-panel', 'big-panel-ai', aiChatOpen ? 'is-open' : 'is-collapsed']">
|
||
<view class="big-panel-head big-panel-head-ai" @tap="toggleAiChat">
|
||
<view class="big-panel-title">AI 对话</view>
|
||
<view class="big-ai-head-actions">
|
||
<view class="big-panel-clear" @tap.stop="clearAiChat">清空</view>
|
||
<view class="big-panel-sub">{{ aiChatOpen ? '收起' : '展开' }}</view>
|
||
</view>
|
||
</view>
|
||
<view v-if="aiChatOpen" class="big-ai-body">
|
||
<scroll-view
|
||
scroll-y
|
||
:scroll-into-view="aiChatScrollInto"
|
||
class="big-ai-messages"
|
||
>
|
||
<view v-if="!aiChatMessages.length" class="big-ai-empty">请输入问题,AI 将为你生成分析建议</view>
|
||
<view
|
||
v-for="(m, idx) in aiChatMessages"
|
||
:key="m.id || idx"
|
||
:class="['big-ai-msg', m.role === 'user' ? 'is-user' : 'is-ai']"
|
||
>
|
||
<view class="big-ai-bubble">{{ m.content }}</view>
|
||
</view>
|
||
<view :id="aiChatBottomId" class="big-ai-bottom"></view>
|
||
</scroll-view>
|
||
<view class="big-ai-input">
|
||
<view
|
||
class="big-ai-voice-btn"
|
||
:class="{ active: voiceMode }"
|
||
@tap="toggleVoiceMode"
|
||
>
|
||
<uni-icons type="mic-filled" size="20" :color="voiceMode ? '#00f0ff' : '#64748B'"></uni-icons>
|
||
</view>
|
||
<input
|
||
class="big-ai-text"
|
||
v-model="aiChatInput"
|
||
:disabled="aiChatSending"
|
||
confirm-type="send"
|
||
placeholder="问:打开张三的报告"
|
||
@confirm="sendAiChat"
|
||
/>
|
||
<view
|
||
class="big-ai-send"
|
||
:class="{ disabled: aiChatSending || !aiChatInputTrim }"
|
||
@tap="sendAiChat"
|
||
>
|
||
发送
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
</view>
|
||
|
||
<view class="big-col">
|
||
<view class="big-panel">
|
||
<view class="big-panel-head">
|
||
<view class="big-panel-title">工具&基础功能</view>
|
||
</view>
|
||
<view class="big-tools">
|
||
<view class="big-tool" @tap="goVoice">
|
||
<view class="big-tool-icon">
|
||
<uni-icons type="mic" size="22" color="#b7f4ff"></uni-icons>
|
||
</view>
|
||
<view class="big-tool-text">语音助手</view>
|
||
</view>
|
||
<view class="big-tool" @tap="goChartTemplates">
|
||
<view class="big-tool-icon">
|
||
<uni-icons type="bars" size="22" color="#b7f4ff"></uni-icons>
|
||
</view>
|
||
<view class="big-tool-text">自定义图表</view>
|
||
</view>
|
||
</view>
|
||
<view class="big-chart">
|
||
<view v-if="bigLoading" class="big-loading">加载中...</view>
|
||
<qiun-data-charts v-else-if="isMounted" :key="'bigLine2-' + chartKey" type="line" :opts="bigLineOpts" :chartData="bigLineData2" canvasId="bigLine2" />
|
||
</view>
|
||
</view>
|
||
|
||
<view class="big-panel big-panel-portrait">
|
||
<view class="big-panel-head">
|
||
<view class="big-panel-title">个体画像</view>
|
||
</view>
|
||
<view class="big-chart">
|
||
<view class="big-portrait-cloud">
|
||
<view
|
||
v-for="(w, idx) in bigPortraitWords"
|
||
:key="w.text + '-' + idx"
|
||
class="big-portrait-word"
|
||
:style="getPortraitWordStyle(w, idx)"
|
||
>
|
||
{{ w.text }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="big-panel big-panel-report">
|
||
<view class="big-panel-head">
|
||
<view class="big-panel-title">综合报告</view>
|
||
</view>
|
||
<view class="big-chart big-chart-xl">
|
||
<qiun-data-charts v-if="isMounted" :key="'bigBarMid-' + chartKey" type="column" :opts="bigBarOpts" :chartData="bigBarData" canvasId="bigBarMid" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
</view>
|
||
|
||
<!-- 登录弹窗 - 放在最后确保在最上层 -->
|
||
<LoginModal :show="showLoginModal" :closable="false" @success="onLoginSuccess" @close="showLoginModal = false" />
|
||
</template>
|
||
|
||
<script>
|
||
import { getToken } from '../../utils/auth'
|
||
import { getUnreadNoticeTop, markNoticeRead } from '../../api/app/notice'
|
||
import { getUnreadMessageList, markMessageRead } from '../../api/app/message'
|
||
import { openLink, getMessageWsUrl } from '../../utils/link'
|
||
import { request, setAuthFailureCallback } from '../../utils/request'
|
||
import { getBaseUrl } from '../../utils/config'
|
||
import { getReport, listReport } from '../../api/psychology/report'
|
||
import { getStudentOptions, getUserAssessmentSummary } from '../../api/psychology/assessment'
|
||
import QiunDataCharts from '../../uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
|
||
import UniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
|
||
import LoginModal from '../../components/LoginModal.vue'
|
||
|
||
export default {
|
||
components: {
|
||
QiunDataCharts,
|
||
UniIcons,
|
||
LoginModal
|
||
},
|
||
// 捕获子组件错误,防止图表错误影响整个应用
|
||
errorCaptured(err, vm, info) {
|
||
// 如果是图表相关的错误,静默处理
|
||
if (err && err.message && (
|
||
err.message.includes('firstElementChild') ||
|
||
err.message.includes('Cannot destructure') ||
|
||
err.message.includes('qiun-data-charts')
|
||
)) {
|
||
console.warn('图表渲染警告(已处理):', err.message)
|
||
// 返回 false 阻止错误继续传播
|
||
return false
|
||
}
|
||
// 其他错误继续传播
|
||
return true
|
||
},
|
||
data() {
|
||
return {
|
||
showLoginModal: false,
|
||
hasCheckedLogin: false,
|
||
isMounted: false,
|
||
chartKey: 0,
|
||
resizeTimer: null,
|
||
socketOpen: false,
|
||
connecting: false,
|
||
voiceTipsOpen: false,
|
||
voiceMode: false,
|
||
recorder: null,
|
||
mediaRecorder: null,
|
||
mediaStream: null,
|
||
audioChunks: [],
|
||
isH5: false,
|
||
centerVideoUrl: '',
|
||
aiChatOpen: true,
|
||
aiChatSending: false,
|
||
aiChatInput: '',
|
||
aiChatMessages: [],
|
||
aiChatScrollInto: '',
|
||
aiChatBottomId: 'aiChatBottom',
|
||
bigLoading: false,
|
||
bigErrorMsg: '',
|
||
bigInboxLoading: false,
|
||
bigInboxList: [],
|
||
bigDeptAssessTotal: 12872,
|
||
bigDeptStats: [],
|
||
bigRingOpts: {
|
||
timing: 'easeOut',
|
||
duration: 900,
|
||
color: ['#2B6BFF', '#00F0FF'],
|
||
padding: [2, 2, 2, 2],
|
||
legend: { show: false },
|
||
extra: {
|
||
ring: {
|
||
ringWidth: 16,
|
||
offsetAngle: -90,
|
||
labelShow: false,
|
||
activeRadius: 6,
|
||
border: true,
|
||
borderWidth: 2,
|
||
borderColor: 'rgba(255,255,255,0.12)'
|
||
}
|
||
}
|
||
},
|
||
bigLineOpts: {
|
||
timing: 'easeOut',
|
||
duration: 1000,
|
||
color: ['#00F0FF', '#2B6BFF'],
|
||
padding: [16, 14, 12, 12],
|
||
legend: {
|
||
show: false,
|
||
position: 'bottom',
|
||
float: 'center',
|
||
fontSize: 12,
|
||
fontColor: 'rgba(220, 250, 255, 0.85)'
|
||
},
|
||
xAxis: {
|
||
disableGrid: false,
|
||
gridType: 'dash',
|
||
dashLength: 6,
|
||
axisLine: true,
|
||
axisLineColor: 'rgba(0, 240, 255, 0.12)',
|
||
fontSize: 12,
|
||
fontColor: 'rgba(220, 250, 255, 0.70)'
|
||
},
|
||
yAxis: {
|
||
min: 0,
|
||
splitNumber: 4,
|
||
gridType: 'dash',
|
||
dashLength: 6,
|
||
fontSize: 12,
|
||
fontColor: 'rgba(220, 250, 255, 0.70)',
|
||
gridColor: 'rgba(0, 240, 255, 0.06)'
|
||
},
|
||
extra: {
|
||
line: {
|
||
type: 'curve',
|
||
width: 2,
|
||
activeType: 'hollow',
|
||
linearType: 'custom',
|
||
linearOpacity: 0.55,
|
||
dataPointShape: 'circle',
|
||
pointSize: 3
|
||
}
|
||
}
|
||
},
|
||
bigPortraitOpts: {
|
||
timing: 'easeOut',
|
||
duration: 1000,
|
||
color: ['#00F0FF'],
|
||
padding: [12, 10, 8, 8],
|
||
legend: { show: false },
|
||
xAxis: {
|
||
disableGrid: false,
|
||
gridType: 'dash',
|
||
dashLength: 6,
|
||
axisLine: true,
|
||
axisLineColor: 'rgba(0, 240, 255, 0.10)',
|
||
fontSize: 11,
|
||
fontColor: 'rgba(220, 250, 255, 0.66)'
|
||
},
|
||
yAxis: {
|
||
min: 0,
|
||
splitNumber: 3,
|
||
gridType: 'dash',
|
||
dashLength: 6,
|
||
fontSize: 11,
|
||
fontColor: 'rgba(220, 250, 255, 0.66)',
|
||
gridColor: 'rgba(0, 240, 255, 0.05)'
|
||
},
|
||
extra: {
|
||
line: {
|
||
type: 'curve',
|
||
width: 2,
|
||
activeType: 'hollow',
|
||
linearType: 'custom',
|
||
linearOpacity: 0.78,
|
||
dataPointShape: 'circle',
|
||
pointSize: 2
|
||
}
|
||
}
|
||
},
|
||
bigPortraitWords: [
|
||
{ text: '自信', size: 44, x: 18, y: 24, color: 'rgba(88, 230, 255, 0.86)' },
|
||
{ text: '积极', size: 56, x: 70, y: 28, color: 'rgba(120, 150, 255, 0.56)' },
|
||
{ text: '乐观', size: 46, x: 56, y: 42, color: 'rgba(197, 142, 255, 0.78)' },
|
||
{ text: '成长', size: 40, x: 34, y: 38, color: 'rgba(88, 230, 255, 0.82)' },
|
||
{ text: '勇气', size: 36, x: 20, y: 46, color: 'rgba(120, 150, 255, 0.48)' },
|
||
{ text: '专注', size: 34, x: 80, y: 44, color: 'rgba(88, 230, 255, 0.72)' },
|
||
{ text: '自律', size: 36, x: 82, y: 58, color: 'rgba(197, 142, 255, 0.64)' },
|
||
{ text: '感恩', size: 32, x: 62, y: 56, color: 'rgba(120, 150, 255, 0.44)' },
|
||
{ text: '善意', size: 34, x: 24, y: 68, color: 'rgba(88, 230, 255, 0.70)' },
|
||
{ text: '同理心', size: 38, x: 44, y: 64, color: 'rgba(197, 142, 255, 0.70)' },
|
||
{ text: '沟通', size: 30, x: 14, y: 80, color: 'rgba(120, 150, 255, 0.40)' },
|
||
{ text: '放松', size: 32, x: 78, y: 76, color: 'rgba(88, 230, 255, 0.64)' },
|
||
{ text: '睡眠', size: 28, x: 68, y: 88, color: 'rgba(120, 150, 255, 0.34)' },
|
||
{ text: '运动', size: 30, x: 34, y: 88, color: 'rgba(88, 230, 255, 0.62)' },
|
||
{ text: '心理韧性', size: 40, x: 52, y: 80, color: 'rgba(197, 142, 255, 0.72)' },
|
||
{ text: '边界感', size: 28, x: 52, y: 26, color: 'rgba(120, 150, 255, 0.34)' },
|
||
{ text: '情绪稳定', size: 34, x: 40, y: 50, color: 'rgba(120, 150, 255, 0.40)' },
|
||
{ text: '目标感', size: 30, x: 60, y: 34, color: 'rgba(88, 230, 255, 0.58)' },
|
||
{ text: '安全感', size: 30, x: 18, y: 92, color: 'rgba(120, 150, 255, 0.30)' },
|
||
{ text: '自我接纳', size: 32, x: 78, y: 92, color: 'rgba(197, 142, 255, 0.46)' }
|
||
],
|
||
bigBarOpts: {
|
||
timing: 'easeOut',
|
||
duration: 900,
|
||
color: ['#00F0FF'],
|
||
padding: [18, 22, 14, 18],
|
||
fontColor: 'rgba(242, 252, 255, 0.92)',
|
||
dataLabel: true,
|
||
legend: {
|
||
show: false
|
||
},
|
||
xAxis: {
|
||
disableGrid: true,
|
||
axisLine: true,
|
||
axisLineColor: 'rgba(0, 240, 255, 0.12)',
|
||
fontSize: 13,
|
||
fontColor: 'rgba(242, 252, 255, 0.90)'
|
||
},
|
||
yAxis: {
|
||
min: 0,
|
||
splitNumber: 4,
|
||
gridType: 'dash',
|
||
dashLength: 4,
|
||
fontSize: 12,
|
||
fontColor: 'rgba(242, 252, 255, 0.78)',
|
||
gridColor: 'rgba(0, 240, 255, 0.08)'
|
||
},
|
||
extra: {
|
||
column: {
|
||
width: 18,
|
||
activeBgColor: '#00F0FF',
|
||
activeBgOpacity: 0.06,
|
||
barBorderRadius: [14, 14, 4, 4],
|
||
linearType: 'custom',
|
||
linearOpacity: 0.95,
|
||
customColor: ['#2B6BFF'],
|
||
colorStop: 0.38
|
||
}
|
||
}
|
||
},
|
||
bigRingData: { series: [{ data: [{ name: '预警', value: 0 }, { name: '正常', value: 0 }] }] },
|
||
bigLineData: { categories: [' '], series: [{ name: '测评次数', data: [0] }] },
|
||
bigLineData2: { categories: [' '], series: [{ name: '报告数', data: [0] }] },
|
||
bigBarData: { categories: [' '], series: [{ name: '报告数', data: [0] }] },
|
||
bigCenterTextTitle: 'AI 心理大数据平台 · 实时态势',
|
||
bigCenterTextDesc: '汇聚测评、预警、干预、画像等核心数据,支持实时研判与联动处置。',
|
||
bigKpi: {
|
||
totalAssessments: 0,
|
||
warningReports: 0,
|
||
tasks: 0,
|
||
pending: 0
|
||
}
|
||
}
|
||
},
|
||
computed: {
|
||
aiChatInputTrim() {
|
||
return String(this.aiChatInput || '').trim()
|
||
}
|
||
},
|
||
onLoad() {
|
||
console.log('=== onLoad 触发 ===')
|
||
|
||
// 注册认证失败回调 - 显示登录弹窗
|
||
setAuthFailureCallback(() => {
|
||
console.log('认证失败回调触发,显示登录弹窗')
|
||
this.showLoginModal = true
|
||
this.bigErrorMsg = ''
|
||
})
|
||
|
||
try {
|
||
const info = uni.getSystemInfoSync()
|
||
console.log('系统信息:', info)
|
||
this.isH5 = info && info.uniPlatform === 'web'
|
||
console.log('isH5:', this.isH5)
|
||
} catch (e) {
|
||
console.error('获取系统信息失败:', e)
|
||
this.isH5 = false
|
||
}
|
||
// 检查登录状态
|
||
this.checkLoginStatus()
|
||
},
|
||
onReady() {
|
||
// 组件渲染完成后延迟标记为已挂载,确保DOM完全准备好
|
||
setTimeout(() => {
|
||
this.$nextTick(() => {
|
||
this.isMounted = true
|
||
})
|
||
}, 300)
|
||
|
||
// 监听窗口大小变化
|
||
// #ifdef H5
|
||
window.addEventListener('resize', this.handleResize)
|
||
// #endif
|
||
},
|
||
onUnload() {
|
||
// 移除窗口大小变化监听
|
||
// #ifdef H5
|
||
window.removeEventListener('resize', this.handleResize)
|
||
// #endif
|
||
|
||
// 清理定时器
|
||
if (this.resizeTimer) {
|
||
clearTimeout(this.resizeTimer)
|
||
}
|
||
},
|
||
onShow() {
|
||
console.log('=== onShow 触发 ===')
|
||
// 检查登录状态
|
||
this.checkLoginStatus()
|
||
},
|
||
methods: {
|
||
handleResize() {
|
||
// 防抖处理:窗口大小变化时,先隐藏图表,等待调整完成后再显示
|
||
if (this.resizeTimer) {
|
||
clearTimeout(this.resizeTimer)
|
||
}
|
||
|
||
try {
|
||
// 暂时隐藏图表,避免resize时的错误
|
||
this.isMounted = false
|
||
|
||
// 1200ms后重新显示图表并更新key强制重新渲染
|
||
this.resizeTimer = setTimeout(() => {
|
||
try {
|
||
this.chartKey++
|
||
// 延迟300ms再显示,确保DOM完全准备好
|
||
setTimeout(() => {
|
||
this.$nextTick(() => {
|
||
this.isMounted = true
|
||
})
|
||
}, 300)
|
||
} catch (e) {
|
||
console.error('图表重新渲染失败:', e)
|
||
// 即使出错也要恢复显示
|
||
this.isMounted = true
|
||
}
|
||
}, 1200)
|
||
} catch (e) {
|
||
console.error('处理窗口大小变化失败:', e)
|
||
// 出错时恢复显示
|
||
this.isMounted = true
|
||
}
|
||
},
|
||
checkLoginStatus() {
|
||
console.log('=== checkLoginStatus 开始 ===')
|
||
const token = getToken()
|
||
console.log('当前token:', token ? '存在' : '不存在')
|
||
console.log('token值:', token)
|
||
console.log('showLoginModal当前状态:', this.showLoginModal)
|
||
|
||
if (!token) {
|
||
// 没有token,显示登录框
|
||
console.log('没有token,准备显示登录框')
|
||
if (!this.showLoginModal) {
|
||
console.log('设置showLoginModal = true')
|
||
this.showLoginModal = true
|
||
this.hasCheckedLogin = true
|
||
} else {
|
||
console.log('登录框已经显示,跳过')
|
||
}
|
||
return false
|
||
}
|
||
|
||
console.log('有token,继续初始化')
|
||
// 有token,标记已检查过登录
|
||
this.hasCheckedLogin = true
|
||
|
||
// 如果是首次加载(onLoad触发),初始化应用
|
||
if (!this.socketOpen && !this.connecting) {
|
||
console.log('初始化应用')
|
||
this.initApp()
|
||
}
|
||
|
||
// 执行其他操作
|
||
this.checkUnreadNoticePopup()
|
||
this.checkUnreadMessagePopup()
|
||
if (this.isH5) {
|
||
this.fetchInboxList()
|
||
}
|
||
|
||
return true
|
||
},
|
||
initApp() {
|
||
if (this.isH5) {
|
||
this.fetchCenterVideo()
|
||
}
|
||
this.initMessageChannel()
|
||
this.checkUnreadNoticePopup()
|
||
this.checkUnreadMessagePopup()
|
||
if (this.isH5) {
|
||
this.fetchBigData()
|
||
this.fetchInboxList()
|
||
}
|
||
},
|
||
onLoginSuccess() {
|
||
console.log('=== 登录成功回调 ===')
|
||
// 关闭登录弹窗
|
||
this.showLoginModal = false
|
||
// 重置登录检查标记
|
||
this.hasCheckedLogin = false
|
||
// 重新检查登录状态并初始化应用
|
||
this.checkLoginStatus()
|
||
},
|
||
getBailianConfig() {
|
||
const HARDCODED_BAILIAN_API_KEY = 'sk-f991fd13fb044abebeaea81b9848c22b'
|
||
let env = null
|
||
try {
|
||
env = (typeof import.meta !== 'undefined' && import.meta && import.meta.env) ? import.meta.env : null
|
||
} catch (e) {
|
||
env = null
|
||
}
|
||
const envKey = env && env.VITE_BAILIAN_API_KEY ? String(env.VITE_BAILIAN_API_KEY) : ''
|
||
const envUrl = env && env.VITE_BAILIAN_API_URL ? String(env.VITE_BAILIAN_API_URL) : ''
|
||
const envModel = env && env.VITE_BAILIAN_MODEL ? String(env.VITE_BAILIAN_MODEL) : ''
|
||
const apiKey = HARDCODED_BAILIAN_API_KEY || envKey || uni.getStorageSync('BAILIAN_API_KEY')
|
||
const apiUrl = (envUrl || uni.getStorageSync('BAILIAN_API_URL')) || 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'
|
||
const model = (envModel || uni.getStorageSync('BAILIAN_MODEL')) || 'qwen-plus'
|
||
if (!apiKey) return null
|
||
return { apiKey, apiUrl, model }
|
||
},
|
||
getOllamaConfig() {
|
||
// 使用DeepSeek官方API
|
||
const apiUrl = 'https://api.deepseek.com/v1/chat/completions'
|
||
const apiKey = 'sk-c8e14faad3be4837a5401a3d02eaf43c'
|
||
const model = 'deepseek-chat'
|
||
return { apiUrl, apiKey, model }
|
||
},
|
||
callBailianChat({ model, apiUrl, apiKey, messages, temperature = 0.3, max_tokens = 1500 }) {
|
||
return new Promise((resolve, reject) => {
|
||
uni.request({
|
||
url: apiUrl,
|
||
method: 'POST',
|
||
header: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${apiKey}`
|
||
},
|
||
data: {
|
||
model,
|
||
messages,
|
||
temperature,
|
||
max_tokens,
|
||
stream: false
|
||
},
|
||
success: (res) => {
|
||
const data = res && res.data ? res.data : null
|
||
const content = data && data.choices && data.choices[0] && data.choices[0].message
|
||
? data.choices[0].message.content
|
||
: ''
|
||
if (!content) {
|
||
reject(new Error('模型返回结果为空'))
|
||
return
|
||
}
|
||
resolve(String(content))
|
||
},
|
||
fail: (err) => reject(err)
|
||
})
|
||
})
|
||
},
|
||
callOllamaChat({ model, apiUrl, messages }) {
|
||
return new Promise((resolve, reject) => {
|
||
const msgList = Array.isArray(messages) ? messages : []
|
||
const extractPlainText = (input) => {
|
||
let s = String(input || '')
|
||
if (!s) return ''
|
||
s = s.replace(/<script[\s\S]*?<\/script>/gi, '')
|
||
s = s.replace(/<style[\s\S]*?<\/style>/gi, '')
|
||
s = s.replace(/<br\s*\/?>/gi, '\n')
|
||
s = s.replace(/<\/?p\b[^>]*>/gi, '\n')
|
||
s = s.replace(/<\/?div\b[^>]*>/gi, '\n')
|
||
s = s.replace(/<[^>]+>/g, '')
|
||
s = s
|
||
.replace(/ /gi, ' ')
|
||
.replace(/</gi, '<')
|
||
.replace(/>/gi, '>')
|
||
.replace(/&/gi, '&')
|
||
.replace(/"/gi, '"')
|
||
.replace(/'/gi, "'")
|
||
s = s.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
||
s = s.replace(/[ \t\f\v]+/g, ' ')
|
||
s = s.replace(/\n\s*\n\s*\n+/g, '\n\n')
|
||
return s.trim()
|
||
}
|
||
const compressMessage = (text, maxLen = 3200) => {
|
||
const s = String(text || '')
|
||
if (!s) return ''
|
||
if (s.length <= maxLen) return s
|
||
const headLen = Math.max(600, Math.floor(maxLen / 2) - 200)
|
||
const tailLen = maxLen - headLen
|
||
const head = s.slice(0, headLen)
|
||
const tail = s.slice(-tailLen)
|
||
return head + '\n...\n' + tail
|
||
}
|
||
const parseContent = (data) => {
|
||
if (typeof data === 'string') return data
|
||
if (!data || typeof data !== 'object') return ''
|
||
// 兼容后端 AjaxResult: { code, msg, data?, content? }
|
||
if (data && data.code != null && (data.content != null || data.data != null)) {
|
||
const c = data.content != null ? data.content : data.data
|
||
if (typeof c === 'string') return c
|
||
if (c && typeof c === 'object') {
|
||
return (
|
||
(c && c.content != null ? String(c.content) : '') ||
|
||
(c && c.message && c.message.content != null ? String(c.message.content) : '')
|
||
)
|
||
}
|
||
}
|
||
return (
|
||
(data && data.message && data.message.content != null ? String(data.message.content) : '') ||
|
||
(data && data.content != null ? String(data.content) : '') ||
|
||
(data && data.answer != null ? String(data.answer) : '') ||
|
||
(data && data.response != null ? String(data.response) : '') ||
|
||
(data && data.data != null ? String(data.data) : '')
|
||
)
|
||
}
|
||
|
||
const mergedMessageFull = msgList
|
||
.map((m) => {
|
||
const role = m && m.role ? String(m.role) : ''
|
||
const content = m && m.content != null ? String(m.content) : ''
|
||
if (!role) return content
|
||
return `${role}: ${content}`
|
||
})
|
||
.filter(Boolean)
|
||
.join('\n')
|
||
|
||
const mergedMessage = compressMessage(extractPlainText(mergedMessageFull))
|
||
|
||
uni.request({
|
||
url: apiUrl,
|
||
method: 'POST',
|
||
header: { 'Content-Type': 'application/json' },
|
||
data: {
|
||
model: String(model || ''),
|
||
message: mergedMessage
|
||
},
|
||
success: (res) => {
|
||
if (res && res.statusCode && res.statusCode >= 400) {
|
||
reject(new Error('模型请求失败: ' + res.statusCode))
|
||
return
|
||
}
|
||
const content = parseContent(res && res.data != null ? res.data : null)
|
||
if (!content) {
|
||
reject(new Error('模型返回为空'))
|
||
return
|
||
}
|
||
resolve(content)
|
||
},
|
||
fail: (err) => reject(err)
|
||
})
|
||
})
|
||
},
|
||
fetchCenterVideo() {
|
||
return request({ url: '/api/homepage/video', method: 'GET' })
|
||
.then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) return
|
||
const url = data.url || (data.data && data.data.url ? data.data.url : '')
|
||
this.centerVideoUrl = url || ''
|
||
})
|
||
.catch(() => {})
|
||
},
|
||
toggleAiChat() {
|
||
this.aiChatOpen = !this.aiChatOpen
|
||
if (this.aiChatOpen) {
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
}
|
||
},
|
||
toggleVoiceMode() {
|
||
this.voiceMode = !this.voiceMode
|
||
if (this.voiceMode) {
|
||
uni.showToast({
|
||
title: '语音对话已开启',
|
||
icon: 'none',
|
||
duration: 1500
|
||
})
|
||
this.startVoiceRecognition()
|
||
} else {
|
||
uni.showToast({
|
||
title: '语音对话已关闭',
|
||
icon: 'none',
|
||
duration: 1500
|
||
})
|
||
this.stopVoiceRecognition()
|
||
}
|
||
},
|
||
startVoiceRecognition() {
|
||
// H5环境下使用浏览器原生录音API
|
||
// #ifdef H5
|
||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||
uni.showToast({
|
||
title: '浏览器不支持录音功能',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
this.voiceMode = false
|
||
return
|
||
}
|
||
|
||
navigator.mediaDevices.getUserMedia({ audio: true })
|
||
.then(stream => {
|
||
console.log('获取麦克风权限成功')
|
||
this.mediaStream = stream
|
||
this.mediaRecorder = new MediaRecorder(stream)
|
||
this.audioChunks = []
|
||
|
||
this.mediaRecorder.ondataavailable = (event) => {
|
||
if (event.data.size > 0) {
|
||
this.audioChunks.push(event.data)
|
||
}
|
||
}
|
||
|
||
this.mediaRecorder.onstop = () => {
|
||
console.log('录音停止')
|
||
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' })
|
||
this.uploadAudioBlob(audioBlob)
|
||
|
||
// 停止所有音轨
|
||
if (this.mediaStream) {
|
||
this.mediaStream.getTracks().forEach(track => track.stop())
|
||
}
|
||
}
|
||
|
||
this.mediaRecorder.start()
|
||
console.log('开始录音...')
|
||
})
|
||
.catch(err => {
|
||
console.error('获取麦克风权限失败:', err)
|
||
uni.showToast({
|
||
title: '无法访问麦克风,请检查权限',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
this.voiceMode = false
|
||
})
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
// 检查是否支持录音
|
||
if (typeof uni.getRecorderManager !== 'function') {
|
||
uni.showToast({
|
||
title: '当前环境不支持录音功能',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
this.voiceMode = false
|
||
return
|
||
}
|
||
|
||
if (!this.recorder) {
|
||
this.recorder = uni.getRecorderManager()
|
||
this.recorder.onStop((res) => {
|
||
console.log('录音停止', res)
|
||
this.onVoiceRecorderStop(res)
|
||
})
|
||
this.recorder.onError((err) => {
|
||
console.error('录音错误', err)
|
||
this.voiceMode = false
|
||
const msg = (err && err.errMsg) ? err.errMsg : '录音失败'
|
||
uni.showToast({
|
||
title: msg,
|
||
icon: 'none'
|
||
})
|
||
})
|
||
this.recorder.onStart(() => {
|
||
console.log('录音开始')
|
||
})
|
||
}
|
||
|
||
console.log('开始录音...')
|
||
this.recorder.start({
|
||
duration: 60000,
|
||
sampleRate: 16000,
|
||
numberOfChannels: 1,
|
||
encodeBitRate: 96000,
|
||
format: 'mp3'
|
||
})
|
||
// #endif
|
||
},
|
||
stopVoiceRecognition() {
|
||
// #ifdef H5
|
||
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
|
||
this.mediaRecorder.stop()
|
||
}
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
if (this.recorder && this.voiceMode) {
|
||
this.recorder.stop()
|
||
}
|
||
// #endif
|
||
},
|
||
onVoiceRecorderStop(res) {
|
||
if (!res || !res.tempFilePath) {
|
||
uni.showToast({
|
||
title: '未获取到录音文件',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
this.uploadAndAsrForAiChat(res.tempFilePath)
|
||
},
|
||
uploadAndAsrForAiChat(tempFilePath) {
|
||
console.log('开始上传音频文件:', tempFilePath)
|
||
uni.showLoading({ title: '识别中...' })
|
||
|
||
const baseUrl = getBaseUrl()
|
||
console.log('上传地址:', baseUrl + '/voice/asr')
|
||
|
||
uni.uploadFile({
|
||
url: baseUrl + '/voice/asr',
|
||
filePath: tempFilePath,
|
||
name: 'file',
|
||
formData: { language: 'zh' },
|
||
header: {
|
||
'Authorization': getToken() || ''
|
||
},
|
||
success: (res) => {
|
||
console.log('上传成功,响应:', res)
|
||
uni.hideLoading()
|
||
let data = null
|
||
try {
|
||
data = JSON.parse(res.data)
|
||
} catch (e) {
|
||
console.error('解析响应失败:', e)
|
||
uni.showToast({
|
||
title: '服务返回格式错误',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
console.log('解析后的数据:', data)
|
||
|
||
if (!data || data.code !== 200) {
|
||
uni.showToast({
|
||
title: (data && data.msg) ? data.msg : '识别失败',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
const recognizedText = data.text || ''
|
||
if (!recognizedText) {
|
||
uni.showToast({
|
||
title: '未识别到有效文本',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
console.log('识别文本:', recognizedText)
|
||
// 将识别的文本设置到输入框并自动发送
|
||
this.aiChatInput = recognizedText
|
||
this.sendAiChat()
|
||
},
|
||
fail: (e) => {
|
||
console.error('上传失败:', e)
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: e && e.errMsg ? e.errMsg : '上传失败',
|
||
icon: 'none'
|
||
})
|
||
}
|
||
})
|
||
},
|
||
uploadAudioBlob(audioBlob) {
|
||
console.log('开始上传音频Blob:', audioBlob)
|
||
uni.showLoading({ title: '识别中...' })
|
||
|
||
const baseUrl = getBaseUrl()
|
||
const formData = new FormData()
|
||
formData.append('file', audioBlob, 'audio.webm')
|
||
formData.append('language', 'zh')
|
||
|
||
fetch(baseUrl + '/voice/asr', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Authorization': getToken() || ''
|
||
},
|
||
body: formData
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
console.log('上传成功,响应:', data)
|
||
uni.hideLoading()
|
||
|
||
if (!data || data.code !== 200) {
|
||
uni.showToast({
|
||
title: (data && data.msg) ? data.msg : '识别失败',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
const recognizedText = data.text || ''
|
||
if (!recognizedText) {
|
||
uni.showToast({
|
||
title: '未识别到有效文本',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
console.log('识别文本:', recognizedText)
|
||
this.aiChatInput = recognizedText
|
||
this.sendAiChat()
|
||
})
|
||
.catch(err => {
|
||
console.error('上传失败:', err)
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '上传失败: ' + err.message,
|
||
icon: 'none'
|
||
})
|
||
})
|
||
},
|
||
scrollAiChatToBottom() {
|
||
// scroll-into-view 需要一个变化的值触发
|
||
this.aiChatScrollInto = this.aiChatBottomId + '-' + Date.now()
|
||
this.$nextTick(() => {
|
||
this.aiChatScrollInto = this.aiChatBottomId
|
||
})
|
||
},
|
||
clearAiChat() {
|
||
this.aiChatMessages = []
|
||
this.aiChatInput = ''
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
},
|
||
matchAiNavigate(text) {
|
||
const raw = String(text || '').trim()
|
||
const s = raw.toLowerCase()
|
||
if (!s) return null
|
||
// 分析类问题不走本地跳转,交给模型分析逻辑
|
||
if (this.isAiAnalyzeQuery(raw)) return null
|
||
const reportOpenMatch = raw.match(/打开\s*([^\s的]{1,32})\s*的?报告/)
|
||
if (reportOpenMatch && reportOpenMatch[1]) {
|
||
const keyword = String(reportOpenMatch[1]).trim()
|
||
return { name: '报告列表', url: '/pages/report/index?keyword=' + encodeURIComponent(keyword) }
|
||
}
|
||
if (/(返回|返回上一页|后退)\s*$/i.test(raw)) {
|
||
return { name: '返回上一页', action: 'navigateBack' }
|
||
}
|
||
if (/(回首页|返回首页|打开首页|首页)\s*$/i.test(raw)) {
|
||
return { name: '首页', action: 'relaunchHome' }
|
||
}
|
||
if (/(清空对话|清空聊天|清空会话|清空记录|重置对话|重置聊天)/.test(raw)) {
|
||
return { name: '清空对话', action: 'clearAiChat' }
|
||
}
|
||
if (/(收起|展开)\s*(ai|助手|对话框)?/i.test(raw) || /(关闭ai|打开ai|关闭助手|打开助手)/i.test(raw)) {
|
||
return { name: '切换AI面板', action: 'toggleAiChat' }
|
||
}
|
||
const createTaskMatch = raw.match(/(?:创建|新建)\s*(?:预警)?\s*任务\s*(\d{1,18})/)
|
||
if (createTaskMatch && createTaskMatch[1]) {
|
||
return { name: '创建预警任务', url: '/pages/warning/createTask?warningId=' + encodeURIComponent(createTaskMatch[1]) }
|
||
}
|
||
if (s.includes('未处理预警') || s.includes('待处理预警')) {
|
||
return { name: '未处理预警', url: '/pages/warning/index?status=0' }
|
||
}
|
||
if (s.includes('处理中预警')) {
|
||
return { name: '处理中预警', url: '/pages/warning/index?status=1' }
|
||
}
|
||
if (s.includes('严重预警') || s.includes('高危预警')) {
|
||
return { name: '严重预警', url: '/pages/warning/index?warningLevel=严重' }
|
||
}
|
||
if (s.includes('高预警')) {
|
||
return { name: '高预警', url: '/pages/warning/index?warningLevel=高' }
|
||
}
|
||
if (s.includes('未完成任务') || s.includes('未完成干预') || s.includes('待处理任务')) {
|
||
return { name: '未完成干预任务', url: '/pages/interventionTask/index?status=0,1' }
|
||
}
|
||
if (s.includes('已完成任务') || s.includes('已完成干预')) {
|
||
return { name: '已完成干预任务', url: '/pages/interventionTask/index?status=2' }
|
||
}
|
||
if (s.includes('设置') || s.includes('我的') || s.includes('个人信息')) {
|
||
return { name: '设置', url: '/pages/settings/index' }
|
||
}
|
||
if (s.includes('收件箱') || s.includes('收件') || s.includes('未读消息') || s.includes('消息列表')) {
|
||
if (s.includes('未读')) return { name: '收件箱未读', url: '/pages/message/inbox?tab=unread' }
|
||
return { name: '收件箱', url: '/pages/message/inbox' }
|
||
}
|
||
if (s.includes('发件箱') || s.includes('发件')) {
|
||
return { name: '发件箱', url: '/pages/message/outbox' }
|
||
}
|
||
if (s.includes('通知') || s.includes('公告')) {
|
||
return { name: '通知公告', url: '/pages/message/notice' }
|
||
}
|
||
if (s.includes('综合报告历史') || s.includes('综合历史') || s.includes('历史综合报告')) {
|
||
return { name: '综合报告历史', url: '/pages/comprehensive/history' }
|
||
}
|
||
if (s.includes('测评报告')) {
|
||
return { name: '测评报告列表', url: '/pages/report/index?sourceType=assessment' }
|
||
}
|
||
if (s.includes('问卷报告')) {
|
||
return { name: '问卷报告列表', url: '/pages/report/index?sourceType=questionnaire' }
|
||
}
|
||
if (s.includes('标签筛选') || s.includes('标签过滤') || s.includes('标签')) {
|
||
return { name: '标签筛选', url: '/pages/profile/tagFilter' }
|
||
}
|
||
if (s.includes('消息发送') || s.includes('发送消息') || s.includes('群发')) {
|
||
return { name: '消息发送', url: '/pages/message/send' }
|
||
}
|
||
const rules = [
|
||
{ name: '预警中心', url: '/pages/warning/index', keys: ['预警', '风险', '告警', '预警中心'] },
|
||
{ name: '监区看板', url: '/pages/dashboard/index', keys: ['看板', '监区', 'dashboard'] },
|
||
{ name: '个体画像', url: '/pages/profile/index', keys: ['画像', '个体', '人员', '档案', 'profile'] },
|
||
{ name: '综合报告', url: '/pages/comprehensive/index', keys: ['综合报告', '综合', 'comprehensive'] },
|
||
{ name: '语音助手', url: '/pages/voice/index', keys: ['语音', '助手', 'voice'] },
|
||
{ name: '自定义图表', url: '/pages/chart/custom', keys: ['自定义图表', '自定义', 'custom'] },
|
||
{ name: '图表模板', url: '/pages/chart/templates', keys: ['图表模板', '模板', '图表', 'charts'] },
|
||
{ name: '干预任务', url: '/pages/interventionTask/index', keys: ['干预', '任务', 'intervention'] },
|
||
{ name: '测评列表', url: '/pages/assessment/index', keys: ['测评', '量表', 'assessment'] },
|
||
{ name: '报告列表', url: '/pages/report/index', keys: ['报告', 'report'] }
|
||
]
|
||
for (let i = 0; i < rules.length; i++) {
|
||
const r = rules[i]
|
||
for (let j = 0; j < r.keys.length; j++) {
|
||
const k = String(r.keys[j]).toLowerCase()
|
||
if (k && s.includes(k)) return { name: r.name, url: r.url }
|
||
}
|
||
}
|
||
return null
|
||
},
|
||
isAiAnalyzeQuery(text) {
|
||
const s = String(text || '').trim()
|
||
if (!s) return false
|
||
return /(分析|趋势|总结|汇总|研判|建议)/.test(s)
|
||
},
|
||
// AI意图识别 - 使用大模型理解用户输入并提取结构化指令
|
||
async parseUserIntentWithAI(userInput) {
|
||
const llmCfg = this.getBailianConfig() || this.getOllamaConfig()
|
||
if (!llmCfg) {
|
||
console.log('AI意图识别:未配置大模型')
|
||
return null
|
||
}
|
||
|
||
// 定义系统可以执行的指令列表
|
||
const availableCommands = {
|
||
navigation: [
|
||
{ action: 'goWarning', params: ['status', 'warningLevel'], desc: '打开预警中心,可选参数:status(0=未处理,1=处理中,2=已完成), warningLevel(严重/高/中/低)' },
|
||
{ action: 'goProfile', params: ['keyword', 'userId'], desc: '打开个体画像,可选参数:keyword(姓名/编号), userId(用户ID)' },
|
||
{ action: 'goComprehensive', params: [], desc: '打开综合报告' },
|
||
{ action: 'goInterventionTask', params: ['status'], desc: '打开干预任务,可选参数:status(0,1=未完成,2=已完成)' },
|
||
{ action: 'goReport', params: ['keyword', 'reportId', 'sourceType'], desc: '打开报告列表或详情,可选参数:keyword(搜索关键词), reportId(报告ID), sourceType(assessment/questionnaire)' },
|
||
{ action: 'goMessage', params: ['tab'], desc: '打开消息/收件箱,可选参数:tab(unread=未读)' },
|
||
{ action: 'goNotice', params: [], desc: '打开通知公告' },
|
||
{ action: 'goDashboard', params: [], desc: '打开监区看板' },
|
||
{ action: 'goTagFilter', params: [], desc: '打开标签筛选' },
|
||
{ action: 'goVoice', params: [], desc: '打开语音助手' }
|
||
],
|
||
analysis: [
|
||
{ action: 'analyzeReport', params: ['keyword', 'reportId'], desc: '分析某人的测评报告/数据,关键词:分析、数据、报告。必需参数:keyword(姓名/编号)或reportId(报告ID)' },
|
||
{ action: 'analyzeProfile', params: ['keyword', 'userId'], desc: '查看某人的个体画像/档案/情况,关键词:画像、档案、情况、了解。必需参数:keyword(姓名/编号)或userId(用户ID)' },
|
||
{ action: 'analyzeData', params: ['dataType'], desc: '分析平台整体数据,关键词:平台数据、统计、趋势。可选参数:dataType(overview=概览/trend=趋势/dept=监区统计)' }
|
||
],
|
||
control: [
|
||
{ action: 'clearChat', params: [], desc: '清空对话记录' },
|
||
{ action: 'toggleChat', params: [], desc: '展开或收起AI对话框' },
|
||
{ action: 'goBack', params: [], desc: '返回上一页' },
|
||
{ action: 'goHome', params: [], desc: '返回首页' }
|
||
]
|
||
}
|
||
|
||
const systemPrompt = `你是一个指令解析助手,负责理解用户的自然语言输入并转换为结构化的系统指令。
|
||
|
||
可用的指令类型:
|
||
${JSON.stringify(availableCommands, null, 2)}
|
||
|
||
重要区分:
|
||
- analyzeReport: 用于"分析XX的报告/数据",查看测评报告详情
|
||
- analyzeProfile: 用于"查看XX的画像/档案/情况",查看个体画像
|
||
- goReport: 用于"打开XX的报告",跳转到报告列表
|
||
|
||
你的任务:
|
||
1. 理解用户输入的意图
|
||
2. 从可用指令中选择最匹配的action
|
||
3. 提取用户输入中的参数值
|
||
4. 返回JSON格式的结构化指令
|
||
|
||
返回格式(必须是有效的JSON):
|
||
{
|
||
"action": "指令名称",
|
||
"params": {
|
||
"参数名": "参数值"
|
||
},
|
||
"confidence": 0.0-1.0,
|
||
"reasoning": "简短说明为什么选择这个指令"
|
||
}
|
||
|
||
如果无法识别用户意图,返回:
|
||
{
|
||
"action": null,
|
||
"confidence": 0,
|
||
"reasoning": "无法理解用户意图"
|
||
}
|
||
|
||
示例:
|
||
用户输入:"打开张三的报告"
|
||
返回:{"action":"goReport","params":{"keyword":"张三"},"confidence":0.95,"reasoning":"用户想查看张三的报告列表"}
|
||
|
||
用户输入:"分析李四的数据"
|
||
返回:{"action":"analyzeReport","params":{"keyword":"李四"},"confidence":0.9,"reasoning":"用户想分析李四的测评报告"}
|
||
|
||
用户输入:"查看王五的画像"
|
||
返回:{"action":"analyzeProfile","params":{"keyword":"王五"},"confidence":0.95,"reasoning":"用户想查看王五的个体画像"}
|
||
|
||
用户输入:"查看未处理的预警"
|
||
返回:{"action":"goWarning","params":{"status":"0"},"confidence":0.95,"reasoning":"用户想查看未处理状态的预警"}
|
||
|
||
重要:只返回JSON,不要有任何其他文字。`
|
||
|
||
try {
|
||
console.log('AI意图识别:开始解析用户输入:', userInput)
|
||
|
||
const messages = [
|
||
{ role: 'system', content: systemPrompt },
|
||
{ role: 'user', content: userInput }
|
||
]
|
||
|
||
const result = await this.callBailianChat({
|
||
model: llmCfg.model,
|
||
apiUrl: llmCfg.apiUrl,
|
||
apiKey: llmCfg.apiKey,
|
||
messages,
|
||
temperature: 0.1, // 低温度,更确定性的输出
|
||
max_tokens: 500
|
||
})
|
||
|
||
// callBailianChat直接返回content字符串,不是完整的result对象
|
||
let content = result || ''
|
||
|
||
console.log('AI意图识别:原始响应:', content)
|
||
console.log('AI意图识别:响应类型:', typeof content)
|
||
console.log('AI意图识别:响应长度:', content.length)
|
||
|
||
// 提取JSON
|
||
let jsonStr = content.trim()
|
||
if (!jsonStr) {
|
||
console.log('AI意图识别:响应为空')
|
||
return null
|
||
}
|
||
|
||
// 移除markdown代码块
|
||
jsonStr = jsonStr.replace(/```(?:json)?/g, '').replace(/```/g, '').trim()
|
||
|
||
// 提取第一个{到最后一个}之间的内容
|
||
const firstBrace = jsonStr.indexOf('{')
|
||
const lastBrace = jsonStr.lastIndexOf('}')
|
||
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
||
jsonStr = jsonStr.substring(firstBrace, lastBrace + 1)
|
||
}
|
||
|
||
console.log('AI意图识别:提取的JSON:', jsonStr)
|
||
|
||
// 解析JSON
|
||
let intent = null
|
||
try {
|
||
intent = JSON.parse(jsonStr)
|
||
} catch(e) {
|
||
console.error('JSON解析失败:', e.message)
|
||
console.log('失败的JSON字符串:', jsonStr)
|
||
return null
|
||
}
|
||
|
||
console.log('AI意图识别:解析结果:', intent)
|
||
|
||
// 验证置信度
|
||
if (intent.confidence < 0.6) {
|
||
console.log('AI意图识别:置信度过低,忽略结果')
|
||
return null
|
||
}
|
||
|
||
return intent
|
||
} catch (e) {
|
||
console.error('AI意图识别失败:', e)
|
||
return null
|
||
}
|
||
},
|
||
parseAnalyzeReportId(text) {
|
||
const s = String(text || '').trim()
|
||
if (!s) return ''
|
||
// 支持多种格式:分析123的报告、分析报告123、分析编号123、分析123
|
||
const m = s.match(/分析\s*(?:报告|编号)?\s*(\d{1,18})(?:\s*的?报告)?/) || s.match(/(\d{1,18})/)
|
||
return m && m[1] ? m[1] : ''
|
||
},
|
||
parseAnalyzeReportKeyword(text) {
|
||
const s = String(text || '').trim()
|
||
if (!s) return ''
|
||
// 支持多种格式:分析张三的报告/数据/画像/档案等
|
||
const m = s.match(/分析\s*([^\s的]{1,32})\s*的?(?:报告|数据|画像|档案|信息)/)
|
||
if (!m || !m[1]) return ''
|
||
const kw = String(m[1]).trim()
|
||
if (!kw || /^\d{1,18}$/.test(kw)) return ''
|
||
return kw
|
||
},
|
||
parseAnalyzeProfileKeyword(text) {
|
||
const s = String(text || '').trim()
|
||
if (!s) return ''
|
||
const m = s.match(/分析\s*([^\s的]{1,32})\s*的?画像/)
|
||
return m && m[1] ? String(m[1]).trim() : ''
|
||
},
|
||
buildAiAnalyzeContext() {
|
||
return {
|
||
overview: this.bigKpi,
|
||
lineAssessTrend: this.bigLineData,
|
||
lineReportTrend: this.bigLineData2,
|
||
deptReportBar: this.bigBarData,
|
||
deptAssessTotal: this.bigDeptAssessTotal
|
||
}
|
||
},
|
||
getAiPortraitFixedMessage() {
|
||
return '心理画像为固定值,无需分析。'
|
||
},
|
||
isPortraitQuery(text) {
|
||
const s = String(text || '').trim()
|
||
if (!s) return false
|
||
return /(心理画像|画像)/.test(s)
|
||
},
|
||
getAiRefuseMessage() {
|
||
return '抱歉,暂不提供此服务。\n你可以尝试:\n- 输入“未处理预警/严重预警/未完成干预/未读消息”等指令\n- 输入“分析XX数据/分析XX的报告/分析报告ID 123/分析张三的画像”等平台内分析指令'
|
||
},
|
||
getAiNotFoundMessage() {
|
||
return '抱歉,没有查询到相关数据。请确认编号/姓名是否正确,或换一个查询条件。'
|
||
},
|
||
isAiInDomain(text) {
|
||
const s = String(text || '').trim()
|
||
if (!s) return false
|
||
return /(预警|风险|告警|干预|任务|测评|量表|报告|画像|综合|趋势|分析|总结|研判|建议|消息|通知|公告|监区|看板|人员|档案)/.test(s)
|
||
},
|
||
isAiNeedContext(text) {
|
||
const s = String(text || '').trim()
|
||
if (!s) return true
|
||
// 仅当属于分析类问题时才允许调用大模型
|
||
return this.isAiAnalyzeQuery(s)
|
||
},
|
||
buildAiSystemPrompt() {
|
||
return '你是“AI心理大数据平台”的内置助手,仅服务于监狱管理员与心理管理员的业务场景。\n' +
|
||
'严格要求:\n' +
|
||
'1) 只能基于用户提供的上下文数据(JSON)进行分析与归纳;禁止编造数据。\n' +
|
||
'2) 只回答平台业务范围:预警、干预任务、测评、报告、消息通知、监区统计。\n' +
|
||
'3) 回复格式:只输出答案本身;不要输出“下一步/建议操作/引导用户去哪里点/平台外链接/参考资料”。\n' +
|
||
'4) 输出风格:中文;简洁;优先给出结论,并用少量要点说明依据;不要长篇铺陈。\n' +
|
||
'5) 心理画像为固定值:禁止对“心理画像/画像词云”等做分析、解释或延伸;如果用户提到心理画像,一律回复“心理画像为固定值,无需分析。”\n' +
|
||
'6) 对平台以外的问题(例如天气、新闻、股票、外部网站、百科、闲聊等)一律回复:\n' +
|
||
'“抱歉,暂不提供此服务。”\n' +
|
||
'7) 如果上下文缺失/无法得出结论,回复:\n' +
|
||
'“抱歉,没有查询到相关数据。”'
|
||
},
|
||
sanitizeAiAnswer(answer, userText) {
|
||
const a = String(answer || '').trim()
|
||
if (!a) return ''
|
||
// 只要用户问题不在平台域内,或回复包含明显平台外内容,则替换为拒答
|
||
const userInDomain = this.isAiInDomain(userText)
|
||
const outHint = /(http(s)?:\/\/|www\.|百度|谷歌|天气|新闻|股票|电影|音乐|游戏|菜谱|百科|维基)/i.test(a)
|
||
if (!userInDomain || outHint) {
|
||
return '抱歉,暂不提供此服务。'
|
||
}
|
||
return a
|
||
},
|
||
canCallAiForText(text) {
|
||
const s = String(text || '').trim()
|
||
if (!s) return false
|
||
if (!this.isAiInDomain(s)) return false
|
||
if (!this.isAiNeedContext(s)) return false
|
||
return true
|
||
},
|
||
// 执行AI识别出的指令
|
||
executeAIIntent(intent) {
|
||
if (!intent || !intent.action) {
|
||
return false
|
||
}
|
||
|
||
const action = intent.action
|
||
const params = intent.params || {}
|
||
|
||
console.log('执行AI指令:', action, params)
|
||
|
||
// 导航类指令
|
||
if (action === 'goWarning') {
|
||
let url = '/pages/warning/index'
|
||
const query = []
|
||
if (params.status) query.push(`status=${params.status}`)
|
||
if (params.warningLevel) query.push(`warningLevel=${encodeURIComponent(params.warningLevel)}`)
|
||
if (query.length) url += '?' + query.join('&')
|
||
uni.navigateTo({ url })
|
||
return true
|
||
}
|
||
|
||
if (action === 'goProfile') {
|
||
let url = '/pages/profile/index'
|
||
const query = []
|
||
if (params.keyword) query.push(`keyword=${encodeURIComponent(params.keyword)}`)
|
||
if (params.userId) query.push(`userId=${params.userId}`)
|
||
if (query.length) url += '?' + query.join('&')
|
||
uni.navigateTo({ url })
|
||
return true
|
||
}
|
||
|
||
if (action === 'goComprehensive') {
|
||
uni.navigateTo({ url: '/pages/comprehensive/index' })
|
||
return true
|
||
}
|
||
|
||
if (action === 'goInterventionTask') {
|
||
let url = '/pages/interventionTask/index'
|
||
if (params.status) url += `?status=${params.status}`
|
||
uni.navigateTo({ url })
|
||
return true
|
||
}
|
||
|
||
if (action === 'goReport') {
|
||
if (params.reportId) {
|
||
uni.navigateTo({ url: `/pages/report/detail?reportId=${params.reportId}&sourceType=${params.sourceType || ''}` })
|
||
} else {
|
||
let url = '/pages/report/index'
|
||
const query = []
|
||
if (params.keyword) query.push(`keyword=${encodeURIComponent(params.keyword)}`)
|
||
if (params.sourceType) query.push(`sourceType=${params.sourceType}`)
|
||
if (query.length) url += '?' + query.join('&')
|
||
uni.navigateTo({ url })
|
||
}
|
||
return true
|
||
}
|
||
|
||
if (action === 'goMessage') {
|
||
let url = '/pages/message/inbox'
|
||
if (params.tab) url += `?tab=${params.tab}`
|
||
uni.navigateTo({ url })
|
||
return true
|
||
}
|
||
|
||
if (action === 'goNotice') {
|
||
uni.navigateTo({ url: '/pages/message/notice' })
|
||
return true
|
||
}
|
||
|
||
if (action === 'goDashboard') {
|
||
uni.navigateTo({ url: '/pages/dashboard/index' })
|
||
return true
|
||
}
|
||
|
||
if (action === 'goTagFilter') {
|
||
uni.navigateTo({ url: '/pages/profile/tagFilter' })
|
||
return true
|
||
}
|
||
|
||
if (action === 'goVoice') {
|
||
uni.navigateTo({ url: '/pages/voice/index' })
|
||
return true
|
||
}
|
||
|
||
// 控制类指令
|
||
if (action === 'clearChat') {
|
||
this.clearAiChat()
|
||
return true
|
||
}
|
||
|
||
if (action === 'toggleChat') {
|
||
this.toggleAiChat()
|
||
return true
|
||
}
|
||
|
||
if (action === 'goBack') {
|
||
uni.navigateBack({ delta: 1 })
|
||
return true
|
||
}
|
||
|
||
if (action === 'goHome') {
|
||
uni.reLaunch({ url: '/pages/index/index' })
|
||
return true
|
||
}
|
||
|
||
// 分析类指令 - 让AI分析数据并返回结果,而不是跳转页面
|
||
if (action === 'analyzeReport' || action === 'analyzeProfile' || action === 'analyzeData') {
|
||
// 返回false,让后续的AI分析逻辑处理
|
||
// 这样会获取数据并调用AI进行分析
|
||
return false
|
||
}
|
||
|
||
return false
|
||
},
|
||
async sendAiChat() {
|
||
if (this.aiChatSending) return
|
||
const text = this.aiChatInputTrim
|
||
if (!text) return
|
||
this.aiChatInput = ''
|
||
const userMsg = { id: 'u_' + Date.now(), role: 'user', content: text }
|
||
this.aiChatMessages = [...this.aiChatMessages, userMsg]
|
||
this.aiChatSending = true
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
|
||
// ===== 新增:AI意图识别 =====
|
||
let aiRecognizedIntent = null
|
||
try {
|
||
console.log('尝试AI意图识别...')
|
||
const intent = await this.parseUserIntentWithAI(text)
|
||
if (intent && intent.action) {
|
||
console.log('AI识别到意图:', intent)
|
||
const executed = this.executeAIIntent(intent)
|
||
if (executed) {
|
||
// 导航类指令已执行,显示反馈
|
||
this.aiChatMessages = [...this.aiChatMessages, {
|
||
id: 'a_' + Date.now(),
|
||
role: 'ai',
|
||
content: `已执行:${intent.reasoning || intent.action}`
|
||
}]
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
return
|
||
}
|
||
// 分析类指令,保存intent供后续使用
|
||
if (intent.action === 'analyzeReport' || intent.action === 'analyzeProfile' || intent.action === 'analyzeData') {
|
||
aiRecognizedIntent = intent
|
||
console.log('分析类指令,保存AI识别的参数:', aiRecognizedIntent)
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('AI意图识别出错,回退到正则匹配:', e)
|
||
}
|
||
// ===== AI意图识别结束 =====
|
||
|
||
const openReportIdMatch = String(text || '').trim().match(/打开\s*(?:报告\s*id\s*|报告\s*#\s*|报告#\s*)(\d{1,18})/i)
|
||
if (openReportIdMatch && openReportIdMatch[1]) {
|
||
const reportId = openReportIdMatch[1]
|
||
uni.navigateTo({ url: `/pages/report/detail?reportId=${encodeURIComponent(reportId)}&sourceType=` })
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: '已跳转:报告详情#' + reportId }]
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
return
|
||
}
|
||
|
||
const openInfoNumberMatch = String(text || '').trim().match(/打开\s*(\d{1,18})\s*的?报告/)
|
||
if (openInfoNumberMatch && openInfoNumberMatch[1]) {
|
||
const infoNumber = openInfoNumberMatch[1]
|
||
uni.navigateTo({ url: `/pages/report/index?keyword=${encodeURIComponent(infoNumber)}` })
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: '已跳转:报告列表(信息编号 ' + infoNumber + ')' }]
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
return
|
||
}
|
||
|
||
const localMatch = this.matchAiNavigate(text)
|
||
if (localMatch && localMatch.action) {
|
||
if (localMatch.action === 'clearAiChat') {
|
||
this.clearAiChat()
|
||
return
|
||
}
|
||
if (localMatch.action === 'toggleAiChat') {
|
||
this.toggleAiChat()
|
||
this.aiChatSending = false
|
||
return
|
||
}
|
||
if (localMatch.action === 'navigateBack') {
|
||
uni.navigateBack({ delta: 1 })
|
||
this.aiChatSending = false
|
||
return
|
||
}
|
||
if (localMatch.action === 'relaunchHome') {
|
||
uni.reLaunch({ url: '/pages/index/index' })
|
||
this.aiChatSending = false
|
||
return
|
||
}
|
||
}
|
||
if (localMatch && localMatch.url) {
|
||
uni.navigateTo({ url: localMatch.url })
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: '已跳转:' + (localMatch.name || localMatch.url) }]
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
return
|
||
}
|
||
|
||
const bailianCfg = this.getBailianConfig()
|
||
const ollamaCfg = this.getOllamaConfig()
|
||
// DeepSeek官方API使用OpenAI兼容格式
|
||
const llmCfg = bailianCfg
|
||
? { type: 'bailian', ...bailianCfg }
|
||
: (ollamaCfg && ollamaCfg.apiKey
|
||
? { type: 'bailian', ...ollamaCfg } // DeepSeek API使用OpenAI格式
|
||
: (ollamaCfg ? { type: 'ollama', ...ollamaCfg } : null))
|
||
if (llmCfg) {
|
||
if (this.isPortraitQuery(text)) {
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiPortraitFixedMessage() }]
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
return
|
||
}
|
||
if (!this.canCallAiForText(text)) {
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiRefuseMessage() }]
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
return
|
||
}
|
||
|
||
// 优先使用AI识别的参数,否则回退到正则匹配
|
||
let reportId = ''
|
||
let reportKeyword = ''
|
||
let profileKeyword = ''
|
||
|
||
if (aiRecognizedIntent) {
|
||
console.log('使用AI识别的参数:', aiRecognizedIntent.params)
|
||
if (aiRecognizedIntent.action === 'analyzeReport') {
|
||
reportId = aiRecognizedIntent.params.reportId || ''
|
||
reportKeyword = aiRecognizedIntent.params.keyword || ''
|
||
} else if (aiRecognizedIntent.action === 'analyzeProfile') {
|
||
profileKeyword = aiRecognizedIntent.params.keyword || aiRecognizedIntent.params.userId || ''
|
||
}
|
||
} else {
|
||
// 回退到正则匹配
|
||
console.log('回退到正则匹配')
|
||
reportId = this.parseAnalyzeReportId(text)
|
||
reportKeyword = this.parseAnalyzeReportKeyword(text)
|
||
profileKeyword = ''
|
||
}
|
||
|
||
console.log('最终使用的参数 - reportId:', reportId, 'reportKeyword:', reportKeyword, 'profileKeyword:', profileKeyword)
|
||
|
||
const baseContext = this.buildAiAnalyzeContext()
|
||
let report = null
|
||
let profile = null
|
||
const p1 = reportId
|
||
? listReport({ pageNum: 1, pageSize: 1, infoNumber: reportId }).then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) return
|
||
const rows = data.rows || []
|
||
if (!Array.isArray(rows) || rows.length === 0) return
|
||
const rid = rows[0].reportId
|
||
if (!rid) return
|
||
return getReport(rid, rows[0].sourceType || '').then((rr) => {
|
||
const dd = rr && rr.data ? rr.data : null
|
||
if (!dd || dd.code !== 200) return
|
||
const r = dd.data || {}
|
||
report = {
|
||
reportId: rid,
|
||
reportTitle: r.reportTitle || r.reportType || '',
|
||
reportContent: r.reportContent || ''
|
||
}
|
||
}).catch(() => {})
|
||
}).catch(() => {})
|
||
: (reportKeyword
|
||
? getStudentOptions({ keyword: reportKeyword, limit: 1 }).then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) return
|
||
const list = data.data || []
|
||
if (!Array.isArray(list) || list.length === 0) return
|
||
const userId = list[0].userId
|
||
if (!userId) return
|
||
return listReport({ pageNum: 1, pageSize: 1, userId }).then((lr) => {
|
||
const d = lr && lr.data ? lr.data : null
|
||
if (!d || d.code !== 200) return
|
||
const rows = d.rows || []
|
||
if (!Array.isArray(rows) || rows.length === 0) return
|
||
const rid = rows[0].reportId
|
||
if (!rid) return
|
||
return getReport(rid, '').then((rr) => {
|
||
const dd = rr && rr.data ? rr.data : null
|
||
if (!dd || dd.code !== 200) return
|
||
const r = dd.data || {}
|
||
report = {
|
||
reportId: rid,
|
||
reportTitle: r.reportTitle || r.reportType || '',
|
||
reportContent: r.reportContent || ''
|
||
}
|
||
}).catch(() => {})
|
||
}).catch(() => {})
|
||
}).catch(() => {})
|
||
: Promise.resolve())
|
||
const p2 = profileKeyword
|
||
? getStudentOptions({ keyword: profileKeyword, limit: 1 }).then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) return
|
||
const list = data.data || []
|
||
if (!Array.isArray(list) || list.length === 0) return
|
||
const userId = list[0].userId
|
||
if (!userId) return
|
||
return getUserAssessmentSummary(userId).then((res2) => {
|
||
const d2 = res2 && res2.data ? res2.data : null
|
||
if (!d2 || d2.code !== 200) return
|
||
profile = { keyword: profileKeyword, userId, summary: d2.data || {} }
|
||
}).catch(() => {})
|
||
}).catch(() => {})
|
||
: Promise.resolve()
|
||
Promise.all([p1, p2]).then(() => {
|
||
// 如果既没有报告也没有画像/概览诉求,则不调用大模型
|
||
if (!reportId && !reportKeyword && !profileKeyword && !/(概览|看板|面板|仪表盘|当前面板|面板数据|当前数据|当前看板|总体|全局)/.test(String(text || ''))) {
|
||
throw new Error('__NO_CONTEXT__')
|
||
}
|
||
if ((reportId || reportKeyword) && report && !report.reportContent) {
|
||
throw new Error('__NOT_FOUND__')
|
||
}
|
||
if (profileKeyword && !profile) {
|
||
throw new Error('__NOT_FOUND__')
|
||
}
|
||
const systemPrompt = this.buildAiSystemPrompt()
|
||
const ctx = { ...baseContext, report, profile }
|
||
const userPayload =
|
||
'用户问题:' + text + '\n\n' +
|
||
'上下文数据(JSON):' + JSON.stringify(ctx)
|
||
const messages = [
|
||
{ role: 'system', content: systemPrompt },
|
||
{ role: 'user', content: userPayload }
|
||
]
|
||
if (llmCfg.type === 'bailian') {
|
||
return this.callBailianChat({
|
||
model: llmCfg.model,
|
||
apiUrl: llmCfg.apiUrl,
|
||
apiKey: llmCfg.apiKey,
|
||
messages
|
||
})
|
||
}
|
||
return this.callOllamaChat({
|
||
model: llmCfg.model,
|
||
apiUrl: llmCfg.apiUrl,
|
||
messages
|
||
})
|
||
}).then((content) => {
|
||
const safe = this.sanitizeAiAnswer(content, text)
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: safe || this.getAiNotFoundMessage() }]
|
||
}).catch((e) => {
|
||
const msg = e && e.message ? e.message : ''
|
||
if (msg === '__NO_CONTEXT__') {
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiRefuseMessage() }]
|
||
return
|
||
}
|
||
if (msg === '__NOT_FOUND__') {
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiNotFoundMessage() }]
|
||
return
|
||
}
|
||
const safe = this.sanitizeAiAnswer(msg, text)
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: safe || '网络错误' }]
|
||
}).finally(() => {
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
})
|
||
return
|
||
}
|
||
|
||
if (this.isAiAnalyzeQuery(text)) {
|
||
if (!this.isAiInDomain(text)) {
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiRefuseMessage() }]
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
return
|
||
}
|
||
const reportId = this.parseAnalyzeReportId(text)
|
||
const profileKeyword = this.parseAnalyzeProfileKeyword(text)
|
||
if (!reportId && !profileKeyword && !/(概览|看板|面板|仪表盘|当前面板|面板数据|当前数据|当前看板|总体|全局)/.test(String(text || ''))) {
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiRefuseMessage() }]
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
return
|
||
}
|
||
const baseContext = this.buildAiAnalyzeContext()
|
||
let report = null
|
||
let profile = null
|
||
|
||
const p1 = reportId
|
||
? listReport({ pageNum: 1, pageSize: 1, infoNumber: reportId }).then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) return
|
||
const rows = data.rows || []
|
||
if (!Array.isArray(rows) || rows.length === 0) return
|
||
const rid = rows[0].reportId
|
||
if (!rid) return
|
||
return getReport(rid, rows[0].sourceType || '').then((rr) => {
|
||
const dd = rr && rr.data ? rr.data : null
|
||
if (!dd || dd.code !== 200) return
|
||
const r = dd.data || {}
|
||
report = {
|
||
reportId: rid,
|
||
reportTitle: r.reportTitle || r.reportType || '',
|
||
reportContent: r.reportContent || ''
|
||
}
|
||
}).catch(() => {})
|
||
}).catch(() => {})
|
||
: Promise.resolve()
|
||
|
||
const p2 = profileKeyword
|
||
? getStudentOptions({ keyword: profileKeyword, limit: 1 }).then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) return
|
||
const list = data.data || []
|
||
if (!Array.isArray(list) || list.length === 0) return
|
||
const userId = list[0].userId
|
||
if (!userId) return
|
||
return getUserAssessmentSummary(userId).then((res2) => {
|
||
const d2 = res2 && res2.data ? res2.data : null
|
||
if (!d2 || d2.code !== 200) return
|
||
profile = { keyword: profileKeyword, userId, summary: d2.data || {} }
|
||
}).catch(() => {})
|
||
}).catch(() => {})
|
||
: Promise.resolve()
|
||
|
||
Promise.all([p1, p2]).then(() => {
|
||
return request({
|
||
url: '/voice/analyze',
|
||
method: 'POST',
|
||
data: { text, context: { ...baseContext, report, profile } }
|
||
})
|
||
}).then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) {
|
||
const msg = (data && data.msg) ? data.msg : 'AI 对话失败'
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: msg }]
|
||
return
|
||
}
|
||
const payload = data.payload || {}
|
||
const content = payload.result || payload.message || data.msg || ''
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: content || '暂无回复' }]
|
||
}).catch((e) => {
|
||
const msg = e && e.message ? e.message : '网络错误'
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: msg }]
|
||
}).finally(() => {
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
})
|
||
return
|
||
}
|
||
|
||
request({
|
||
url: '/voice/command',
|
||
method: 'POST',
|
||
data: { text }
|
||
}).then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (data && data.code === 200 && data.type === 'navigate' && data.payload && data.payload.url) {
|
||
uni.navigateTo({ url: data.payload.url })
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: '已跳转:' + data.payload.url }]
|
||
return true
|
||
}
|
||
return false
|
||
}).then((navigated) => {
|
||
if (navigated) return
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: '暂时无法回答,请尝试“分析XX数据”或输入可跳转指令。' }]
|
||
}).catch((e) => {
|
||
const msg = e && e.message ? e.message : '网络错误'
|
||
this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: msg }]
|
||
}).finally(() => {
|
||
this.aiChatSending = false
|
||
this.$nextTick(() => {
|
||
this.scrollAiChatToBottom()
|
||
})
|
||
})
|
||
},
|
||
getPortraitWordStyle(w, idx) {
|
||
const baseLeft = typeof w.x === 'number' ? w.x : 50
|
||
const baseTop = typeof w.y === 'number' ? w.y : 50
|
||
const size = typeof w.size === 'number' ? w.size : 28
|
||
const hue = idx % 3
|
||
const anim = (idx % 4) + 1
|
||
const delay = (idx % 7) * 0.18
|
||
const drift = 0.6 + (idx % 5) * 0.18
|
||
const fallbackColor =
|
||
hue === 0
|
||
? 'rgba(88, 230, 255, 0.86)'
|
||
: hue === 1
|
||
? 'rgba(197, 142, 255, 0.56)'
|
||
: 'rgba(120, 150, 255, 0.52)'
|
||
return {
|
||
left: baseLeft + '%',
|
||
top: baseTop + '%',
|
||
fontSize: size + 'rpx',
|
||
color: w.color || fallbackColor,
|
||
opacity: w.opacity != null ? w.opacity : 1,
|
||
transform: 'translate(-50%, -50%)',
|
||
animationName: 'portraitFloat' + anim,
|
||
animationDuration: drift.toFixed(2) + 's',
|
||
animationTimingFunction: 'ease-in-out',
|
||
animationIterationCount: 'infinite',
|
||
animationDelay: delay.toFixed(2) + 's'
|
||
}
|
||
},
|
||
fetchBigData() {
|
||
// 检查是否有token,没有token不发起请求
|
||
const token = getToken()
|
||
if (!token) {
|
||
console.log('fetchBigData: 没有token,跳过请求')
|
||
this.applyBigDemoData()
|
||
return
|
||
}
|
||
|
||
console.log('fetchBigData: 开始请求,token存在')
|
||
if (this.bigLoading) return
|
||
this.bigLoading = true
|
||
this.bigErrorMsg = ''
|
||
Promise.all([
|
||
request({ url: '/psychology/assessment/analytics', method: 'GET' }),
|
||
request({ url: '/psychology/assessment/deptOverview?topN=7', method: 'GET' })
|
||
])
|
||
.then(([aRes, dRes]) => {
|
||
this.bigLoading = false
|
||
console.log('fetchBigData: analytics响应 code:', aRes.data?.code)
|
||
const aData = aRes && aRes.data ? aRes.data : null
|
||
const dData = dRes && dRes.data ? dRes.data : null
|
||
|
||
// 认证失败已在request.js中统一处理,这里只处理其他错误
|
||
if (!aData || aData.code !== 200) {
|
||
const errorMsg = (aData && aData.msg) ? aData.msg : '概览加载失败'
|
||
console.error('fetchBigData: 请求失败,code:', aData ? aData.code : 'null')
|
||
// 如果是认证失败,不显示错误消息(已在request.js中处理)
|
||
if (aData && (aData.code === 401 || aData.code === 403)) {
|
||
this.bigErrorMsg = ''
|
||
} else {
|
||
this.bigErrorMsg = errorMsg
|
||
}
|
||
this.applyBigDemoData()
|
||
return
|
||
}
|
||
const analytics = aData.data || {}
|
||
const overview = analytics.overview || {}
|
||
this.bigRingData = this.buildRingData(overview)
|
||
this.bigLineData = this.buildTrendLineData(analytics)
|
||
this.bigLineData2 = this.buildTrendReportLineData(analytics)
|
||
const deptRows = (dData && dData.code === 200) ? (dData.data || []) : []
|
||
this.bigBarData = this.buildDeptBarData(deptRows)
|
||
this.bigDeptAssessTotal = this.buildDeptAssessTotal(deptRows, overview)
|
||
this.bigDeptStats = this.buildDeptStats(deptRows)
|
||
this.bigKpi = this.buildKpi(overview)
|
||
})
|
||
.catch((e) => {
|
||
this.bigLoading = false
|
||
console.error('fetchBigData: 请求异常', e)
|
||
this.bigErrorMsg = e && e.message ? e.message : '网络错误'
|
||
this.applyBigDemoData()
|
||
})
|
||
},
|
||
applyBigDemoData() {
|
||
const emptyOverview = {}
|
||
const emptyAnalytics = {}
|
||
this.bigRingData = this.buildRingData(emptyOverview)
|
||
this.bigLineData = this.buildTrendLineData(emptyAnalytics)
|
||
this.bigLineData2 = this.buildTrendReportLineData(emptyAnalytics)
|
||
this.bigBarData = this.buildDeptBarData([])
|
||
this.bigDeptAssessTotal = this.buildDeptAssessTotal([], emptyOverview)
|
||
this.bigDeptStats = this.buildDeptStats([])
|
||
this.bigKpi = this.buildKpi(emptyOverview)
|
||
},
|
||
buildDeptStats(rows) {
|
||
const list = Array.isArray(rows) ? rows : []
|
||
if (!list.length) {
|
||
return [
|
||
{ name: '一监区', value: 18 },
|
||
{ name: '二监区', value: 26 },
|
||
{ name: '三监区', value: 14 },
|
||
{ name: '四监区', value: 32 },
|
||
{ name: '五监区', value: 22 },
|
||
{ name: '六监区', value: 28 }
|
||
]
|
||
}
|
||
const mapped = list
|
||
.map((r) => {
|
||
const name = r.deptName || String(r.deptId || '')
|
||
const v = Number(r.totalAssessments || r.assessments || r.assessmentCount || r.testCount || r.total || r.generatedReports || 0)
|
||
return { name, value: isNaN(v) ? 0 : v }
|
||
})
|
||
.filter((it) => it.name)
|
||
return mapped.slice(0, 10)
|
||
},
|
||
buildDeptAssessTotal(rows, overview) {
|
||
const list = Array.isArray(rows) ? rows : []
|
||
if (list.length) {
|
||
const sum = list.reduce((acc, r) => {
|
||
const v = Number(
|
||
r.totalAssessments || r.assessments || r.assessmentCount || r.testCount || r.total || 0
|
||
)
|
||
return acc + (isNaN(v) ? 0 : v)
|
||
}, 0)
|
||
if (sum > 0) return sum
|
||
}
|
||
const fallback = Number(overview && (overview.totalAssessments || overview.assessmentTotal || overview.testTotal) || 0)
|
||
if (fallback > 0) return fallback
|
||
return 12872
|
||
},
|
||
formatBigNumber(n) {
|
||
const num = Number(n || 0)
|
||
if (!num) return '0'
|
||
return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||
},
|
||
buildRingData(overview) {
|
||
const total = Number(overview.generatedReports || 0)
|
||
const warning = Number(overview.warningReports || 0)
|
||
const normal = Math.max(total - warning, 0)
|
||
if (total <= 0 && warning <= 0 && normal <= 0) {
|
||
return {
|
||
series: [{
|
||
data: [
|
||
{ name: '预警', value: 12 },
|
||
{ name: '正常', value: 88 }
|
||
]
|
||
}]
|
||
}
|
||
}
|
||
return {
|
||
series: [{
|
||
data: [
|
||
{ name: '预警', value: warning },
|
||
{ name: '正常', value: normal }
|
||
]
|
||
}]
|
||
}
|
||
},
|
||
buildTrendLineData(analytics) {
|
||
const list = analytics && analytics.monthlyTrend ? analytics.monthlyTrend : []
|
||
if (!list || !list.length) {
|
||
const categories = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
|
||
const data = [32, 45, 38, 52, 48, 66, 58, 74, 60, 82, 76, 90]
|
||
return { categories, series: [{ name: '测评次数', data }] }
|
||
}
|
||
const categories = list.map((it) => it.label)
|
||
const data = list.map((it) => Number(it.value || 0))
|
||
return { categories, series: [{ name: '测评次数', data }] }
|
||
},
|
||
buildTrendReportLineData(analytics) {
|
||
const list = (analytics && analytics.monthlyReportTrend)
|
||
? analytics.monthlyReportTrend
|
||
: ((analytics && analytics.deptReportTrend) ? analytics.deptReportTrend : [])
|
||
if (!list || !list.length) {
|
||
const categories = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
|
||
const data = [6, 10, 8, 12, 9, 14, 11, 16, 13, 18, 15, 20]
|
||
return { categories, series: [{ name: '报告数', data }] }
|
||
}
|
||
const categories = list.map((it) => it.label)
|
||
const data = list.map((it) => Number(it.value || 0))
|
||
return { categories, series: [{ name: '报告数', data }] }
|
||
},
|
||
buildDeptBarData(rows) {
|
||
const list = Array.isArray(rows) ? rows : []
|
||
if (!list.length) {
|
||
const categories = ['一监区', '二监区', '三监区', '四监区', '五监区', '六监区']
|
||
const data = [18, 26, 14, 32, 22, 28]
|
||
return { categories, series: [{ name: '报告数', data }] }
|
||
}
|
||
const categories = list.map((r) => r.deptName || String(r.deptId || ''))
|
||
const data = list.map((r) => Number(r.generatedReports || 0))
|
||
return { categories, series: [{ name: '报告数', data }] }
|
||
},
|
||
buildKpi(overview) {
|
||
const totalAssessments = Number(overview.totalAssessments || 0)
|
||
const warningReports = Number(overview.warningReports || 0)
|
||
const tasks = Number(overview.interventionTasks || 0)
|
||
const pending = Number(overview.pendingTasks || 0)
|
||
if (totalAssessments <= 0 && warningReports <= 0 && tasks <= 0 && pending <= 0) {
|
||
return {
|
||
totalAssessments: 1287,
|
||
warningReports: 36,
|
||
tasks: 19,
|
||
pending: 7
|
||
}
|
||
}
|
||
return { totalAssessments, warningReports, tasks, pending }
|
||
},
|
||
toggleVoiceTips() {
|
||
this.voiceTipsOpen = !this.voiceTipsOpen
|
||
},
|
||
fetchInboxList() {
|
||
// 检查是否有token,没有token不发起请求
|
||
const token = getToken()
|
||
if (!token) {
|
||
console.log('fetchInboxList: 没有token,跳过请求')
|
||
this.bigInboxList = []
|
||
return
|
||
}
|
||
|
||
if (this.bigInboxLoading) return
|
||
this.bigInboxLoading = true
|
||
getUnreadMessageList(6)
|
||
.then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) return
|
||
const list = data.data || []
|
||
this.bigInboxList = Array.isArray(list) ? list.slice(0, 6) : []
|
||
})
|
||
.catch(() => {
|
||
this.bigInboxList = []
|
||
})
|
||
.finally(() => {
|
||
this.bigInboxLoading = false
|
||
})
|
||
},
|
||
openInboxItem(m) {
|
||
if (!m) return
|
||
const msgId = m.msgId
|
||
const title = m.title || '消息'
|
||
const linkUrl = m.linkUrl || ''
|
||
if (msgId) {
|
||
markMessageRead(msgId).catch(() => {})
|
||
}
|
||
if (linkUrl) {
|
||
openLink(linkUrl, { title })
|
||
return
|
||
}
|
||
this.showMessagePopup(m)
|
||
},
|
||
checkUnreadMessagePopup() {
|
||
getUnreadMessageList(1)
|
||
.then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) return
|
||
const list = data.data || []
|
||
if (!Array.isArray(list) || !list.length) return
|
||
this.showMessagePopup(list[0])
|
||
})
|
||
.catch(() => {})
|
||
},
|
||
initMessageChannel() {
|
||
if (this.socketOpen || this.connecting) return
|
||
const token = getToken()
|
||
if (!token) return
|
||
const wsUrl = getMessageWsUrl(token)
|
||
if (!wsUrl) return
|
||
this.connecting = true
|
||
uni.connectSocket({ url: wsUrl })
|
||
uni.onSocketOpen(() => {
|
||
this.socketOpen = true
|
||
this.connecting = false
|
||
})
|
||
uni.onSocketClose(() => {
|
||
this.socketOpen = false
|
||
this.connecting = false
|
||
})
|
||
uni.onSocketError(() => {
|
||
this.socketOpen = false
|
||
this.connecting = false
|
||
})
|
||
uni.onSocketMessage((res) => {
|
||
this.handleSocketMessage(res)
|
||
})
|
||
},
|
||
handleSocketMessage(res) {
|
||
let data = null
|
||
try {
|
||
data = res && res.data ? JSON.parse(res.data) : null
|
||
} catch (e) {
|
||
data = null
|
||
}
|
||
if (!data || !data.type) return
|
||
if (data.type === 'notice:new') {
|
||
this.showNoticePopup(data.payload)
|
||
return
|
||
}
|
||
if (data.type === 'message:new') {
|
||
this.showMessagePopup(data.payload)
|
||
return
|
||
}
|
||
},
|
||
checkUnreadNoticePopup() {
|
||
getUnreadNoticeTop(1)
|
||
.then((res) => {
|
||
const data = res && res.data ? res.data : null
|
||
if (!data || data.code !== 200) return
|
||
const notice = data.data
|
||
if (!notice || !notice.noticeId) return
|
||
this.showNoticePopup(notice)
|
||
})
|
||
.catch(() => {})
|
||
},
|
||
showNoticePopup(notice) {
|
||
if (!notice) return
|
||
const noticeId = notice.noticeId
|
||
const title = notice.title || '通知'
|
||
const content = notice.content || ''
|
||
const linkUrl = notice.linkUrl || ''
|
||
uni.showModal({
|
||
title,
|
||
content,
|
||
confirmText: linkUrl ? '查看' : '知道了',
|
||
cancelText: '关闭',
|
||
success: (r) => {
|
||
if (noticeId) {
|
||
markNoticeRead(noticeId).catch(() => {})
|
||
}
|
||
if (r && r.confirm && linkUrl) {
|
||
openLink(linkUrl, { title })
|
||
}
|
||
}
|
||
})
|
||
},
|
||
showMessagePopup(msg) {
|
||
if (!msg) return
|
||
const msgId = msg.msgId
|
||
const title = msg.title || '消息'
|
||
const content = msg.content || ''
|
||
const linkUrl = msg.linkUrl || ''
|
||
const bizType = msg.bizType || ''
|
||
if (bizType === 'warning:urgent') {
|
||
try {
|
||
if (typeof window !== 'undefined' && window.AndroidTTS && typeof window.AndroidTTS.speak === 'function') {
|
||
window.AndroidTTS.speak(content)
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
uni.showModal({
|
||
title,
|
||
content,
|
||
confirmText: linkUrl ? '查看' : '知道了',
|
||
cancelText: '关闭',
|
||
success: (r) => {
|
||
if (msgId) {
|
||
markMessageRead(msgId).catch(() => {})
|
||
}
|
||
if (r && r.confirm && linkUrl) {
|
||
openLink(linkUrl, { title })
|
||
}
|
||
}
|
||
})
|
||
},
|
||
goSentenceCrimeAnalysis() {
|
||
uni.navigateTo({
|
||
url: '/pages/chart/sentenceCrimeAnalysis'
|
||
})
|
||
},
|
||
goInterventionTasks() {
|
||
uni.navigateTo({
|
||
url: '/pages/interventionTask/index'
|
||
})
|
||
},
|
||
goChartTemplates() {
|
||
uni.navigateTo({
|
||
url: '/pages/chart/templates'
|
||
})
|
||
},
|
||
goTagFilter() {
|
||
uni.navigateTo({
|
||
url: '/pages/profile/tagFilter'
|
||
})
|
||
},
|
||
goComprehensive() {
|
||
uni.navigateTo({
|
||
url: '/pages/comprehensive/index'
|
||
})
|
||
},
|
||
goVoice() {
|
||
uni.navigateTo({
|
||
url: '/pages/voice/index'
|
||
})
|
||
},
|
||
goWarning() {
|
||
uni.navigateTo({
|
||
url: '/pages/warning/index'
|
||
})
|
||
},
|
||
goProfile() {
|
||
uni.navigateTo({
|
||
url: '/pages/profile/index'
|
||
})
|
||
},
|
||
goDashboard() {
|
||
uni.navigateTo({
|
||
url: '/pages/dashboard/index'
|
||
})
|
||
},
|
||
goNotice() {
|
||
uni.navigateTo({
|
||
url: '/pages/message/notice'
|
||
})
|
||
},
|
||
goSettings() {
|
||
uni.navigateTo({
|
||
url: '/pages/settings/index'
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 自适应字体大小变量 - 基于视口宽度 */
|
||
:root {
|
||
--font-xs: calc(22rpx + 0.3vw); /* 小字体 */
|
||
--font-sm: calc(24rpx + 0.35vw); /* 小字体 */
|
||
--font-base: calc(26rpx + 0.4vw); /* 基础字体 */
|
||
--font-md: calc(28rpx + 0.45vw); /* 中等字体 */
|
||
--font-lg: calc(32rpx + 0.6vw); /* 大字体 */
|
||
--font-xl: calc(38rpx + 0.75vw); /* 超大字体 */
|
||
--font-2xl: calc(56rpx + 1.2vw); /* 特大字体 */
|
||
--font-3xl: calc(60rpx + 1.5vw); /* 巨大字体 */
|
||
}
|
||
|
||
.page {
|
||
min-height: 100vh;
|
||
padding: 24rpx 24rpx 0;
|
||
box-sizing: border-box;
|
||
background: #F4F6FB;
|
||
--c-primary: #1677ff;
|
||
--c-danger: #E87A7A;
|
||
/* 页面级字体变量 */
|
||
--font-xs: calc(22rpx + 0.3vw);
|
||
--font-sm: calc(24rpx + 0.35vw);
|
||
--font-base: calc(26rpx + 0.4vw);
|
||
--font-md: calc(28rpx + 0.45vw);
|
||
--font-lg: calc(32rpx + 0.6vw);
|
||
--font-xl: calc(38rpx + 0.75vw);
|
||
--font-2xl: calc(56rpx + 1.2vw);
|
||
--font-3xl: calc(60rpx + 1.5vw);
|
||
}
|
||
|
||
.section {
|
||
margin-top: 14rpx;
|
||
}
|
||
.section-title {
|
||
font-size: var(--font-md);
|
||
font-weight: 700;
|
||
color: #111827;
|
||
margin: 10rpx 6rpx 14rpx;
|
||
}
|
||
|
||
.grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.card {
|
||
width: 48%;
|
||
background: #FFFFFF;
|
||
border-radius: 16rpx;
|
||
padding: 22rpx 20rpx;
|
||
box-sizing: border-box;
|
||
margin-bottom: 18rpx;
|
||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||
box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.05);
|
||
transition: transform 120ms ease, border-color 120ms ease;
|
||
}
|
||
.card-wide {
|
||
width: 100%;
|
||
}
|
||
.card:active {
|
||
transform: scale(0.98);
|
||
border-color: rgba(22, 119, 255, 0.35);
|
||
}
|
||
|
||
.card-core {
|
||
position: relative;
|
||
overflow: hidden;
|
||
padding: 26rpx 22rpx;
|
||
}
|
||
.card-sub {
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.card-warning {
|
||
background: #FFFFFF;
|
||
border: 1px solid rgba(232, 122, 122, 0.18);
|
||
box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.05);
|
||
}
|
||
.card-profile {
|
||
background: #FFFFFF;
|
||
border: 1px solid rgba(22, 119, 255, 0.18);
|
||
box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.05);
|
||
}
|
||
|
||
.card-accent {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 10rpx;
|
||
border-top-left-radius: 16rpx;
|
||
border-bottom-left-radius: 16rpx;
|
||
}
|
||
.card-warning .card-accent {
|
||
background: linear-gradient(180deg, rgba(232, 122, 122, 0.95) 0%, rgba(232, 122, 122, 0.55) 100%);
|
||
}
|
||
.card-profile .card-accent {
|
||
background: linear-gradient(180deg, rgba(22, 119, 255, 0.95) 0%, rgba(22, 119, 255, 0.55) 100%);
|
||
}
|
||
.card-accent-primary {
|
||
background: linear-gradient(180deg, rgba(22, 119, 255, 0.95) 0%, rgba(22, 119, 255, 0.55) 100%);
|
||
}
|
||
.card-accent-blue {
|
||
background: linear-gradient(180deg, rgba(59, 130, 246, 0.95) 0%, rgba(59, 130, 246, 0.55) 100%);
|
||
}
|
||
.card-accent-purple {
|
||
background: linear-gradient(180deg, rgba(111, 99, 217, 0.95) 0%, rgba(111, 99, 217, 0.55) 100%);
|
||
}
|
||
.card-accent-orange {
|
||
background: linear-gradient(180deg, rgba(245, 158, 11, 0.95) 0%, rgba(245, 158, 11, 0.55) 100%);
|
||
}
|
||
.card-accent-slate {
|
||
background: linear-gradient(180deg, rgba(99, 102, 241, 0.95) 0%, rgba(99, 102, 241, 0.55) 100%);
|
||
}
|
||
|
||
.card-analysis,
|
||
.card-tools,
|
||
.card-group {
|
||
background: #FFFFFF;
|
||
}
|
||
|
||
.card-icon {
|
||
width: calc(64rpx + 1.5vh);
|
||
height: calc(64rpx + 1.5vh);
|
||
border-radius: 16rpx;
|
||
margin-bottom: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(15, 23, 42, 0.06);
|
||
}
|
||
.card-icon-solid {
|
||
background: rgba(22, 119, 255, 0.92);
|
||
}
|
||
.card-warning .card-icon-solid {
|
||
background: rgba(232, 122, 122, 0.92);
|
||
}
|
||
.card-profile .card-icon-solid {
|
||
background: rgba(22, 119, 255, 0.92);
|
||
}
|
||
.card-purple .card-icon {
|
||
background: rgba(111, 99, 217, 0.12);
|
||
}
|
||
.card-blue .card-icon {
|
||
background: rgba(59, 130, 246, 0.12);
|
||
}
|
||
.card-tools .card-icon {
|
||
background: rgba(22, 119, 255, 0.10);
|
||
}
|
||
.card-orange .card-icon {
|
||
background: rgba(245, 158, 11, 0.12);
|
||
}
|
||
.card-group .card-icon {
|
||
background: rgba(99, 102, 241, 0.12);
|
||
}
|
||
.card-icon-inner {
|
||
line-height: 1;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: var(--font-lg);
|
||
font-weight: 700;
|
||
color: #111827;
|
||
}
|
||
|
||
.card-desc {
|
||
margin-top: 10rpx;
|
||
font-size: var(--font-sm);
|
||
color: #6B7280;
|
||
}
|
||
|
||
.fold {
|
||
margin-top: 16rpx;
|
||
border-radius: 14rpx;
|
||
background: rgba(255, 255, 255, 0.7);
|
||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||
overflow: hidden;
|
||
}
|
||
.fold-header {
|
||
padding: 14rpx 14rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.fold-title {
|
||
font-size: var(--font-sm);
|
||
font-weight: 700;
|
||
color: #334155;
|
||
}
|
||
.fold-body {
|
||
padding: 0 14rpx 14rpx;
|
||
}
|
||
.fold-item {
|
||
font-size: var(--font-sm);
|
||
color: #475569;
|
||
line-height: 40rpx;
|
||
}
|
||
|
||
.mini-actions {
|
||
margin-top: 16rpx;
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
flex-wrap: nowrap;
|
||
gap: 12rpx;
|
||
}
|
||
.mini {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding: 12rpx 10rpx;
|
||
border-radius: 14rpx;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
border: 1px solid rgba(15, 23, 42, 0.06);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
}
|
||
.mini:active {
|
||
transform: scale(0.98);
|
||
}
|
||
.mini-text {
|
||
font-size: var(--font-xs);
|
||
color: #334155;
|
||
}
|
||
|
||
.card.disabled {
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.page.big {
|
||
min-height: 100vh;
|
||
padding: 12rpx 12rpx 20rpx;
|
||
box-sizing: border-box;
|
||
position: relative;
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
--neon-a: rgba(0, 240, 255, 0.95);
|
||
--neon-b: rgba(60, 140, 255, 0.95);
|
||
--neon-soft: rgba(0, 240, 255, 0.18);
|
||
--glass: rgba(7, 13, 28, 0.42);
|
||
--glass-2: rgba(10, 18, 38, 0.52);
|
||
background: #020610;
|
||
color: rgba(242, 252, 255, 0.96);
|
||
font-size: var(--font-sm);
|
||
line-height: 1.5;
|
||
text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.55);
|
||
}
|
||
.big-center-media {
|
||
width: 100%;
|
||
height: calc(74vh - 360rpx);
|
||
max-height: calc(720rpx + 7vh);
|
||
border-radius: 18rpx;
|
||
overflow: hidden;
|
||
}
|
||
.big-center-video {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
display: block;
|
||
}
|
||
.big-center-bg {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 18rpx;
|
||
overflow: hidden;
|
||
pointer-events: none;
|
||
background-image: url('/static/bg.png');
|
||
background-size: cover;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
opacity: 0.96;
|
||
filter: saturate(1.08) contrast(1.08);
|
||
}
|
||
.big-center-text {
|
||
border-radius: 18rpx;
|
||
border: 1px solid rgba(0, 240, 255, 0.14);
|
||
background: linear-gradient(180deg, rgba(7, 13, 28, 0.62) 0%, rgba(7, 13, 28, 0.46) 100%);
|
||
box-shadow: 0 12rpx 24rpx rgba(0, 0, 0, 0.35);
|
||
backdrop-filter: blur(6px);
|
||
padding: 16rpx 18rpx;
|
||
}
|
||
.big-center-text-title {
|
||
font-size: var(--font-base);
|
||
font-weight: 900;
|
||
color: rgba(220, 250, 255, 0.94);
|
||
letter-spacing: 1rpx;
|
||
text-shadow: 0 0 16rpx rgba(0, 240, 255, 0.16);
|
||
}
|
||
.big-center-text-desc {
|
||
margin-top: 10rpx;
|
||
font-size: var(--font-xs);
|
||
line-height: 32rpx;
|
||
color: rgba(201, 242, 255, 0.72);
|
||
}
|
||
.big-panel-inbox {
|
||
margin-top: 16rpx;
|
||
min-height: calc(320rpx + 2vh);
|
||
}
|
||
.big-panel-portrait .big-chart {
|
||
height: calc(420rpx + 3vh);
|
||
}
|
||
.big-inbox-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
}
|
||
.big-inbox-item {
|
||
border-radius: 14rpx;
|
||
padding: 14rpx 14rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.18);
|
||
background: rgba(7, 13, 28, 0.78);
|
||
box-sizing: border-box;
|
||
}
|
||
.big-inbox-title {
|
||
font-size: var(--font-sm);
|
||
font-weight: 900;
|
||
color: rgba(242, 252, 255, 0.92);
|
||
}
|
||
.big-inbox-desc {
|
||
margin-top: 8rpx;
|
||
font-size: var(--font-xs);
|
||
color: rgba(242, 252, 255, 0.84);
|
||
line-height: 28rpx;
|
||
max-height: calc(56rpx + 0.5vh);
|
||
overflow: hidden;
|
||
}
|
||
.page.big:before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
background-image:
|
||
linear-gradient(0deg, rgba(0, 240, 255, 0.12) 1px, rgba(0, 0, 0, 0) 1px),
|
||
linear-gradient(90deg, rgba(60, 140, 255, 0.10) 1px, rgba(0, 0, 0, 0) 1px),
|
||
linear-gradient(135deg, rgba(0, 240, 255, 0.10) 0%, rgba(165, 90, 255, 0.10) 100%),
|
||
radial-gradient(circle at 20% 18%, rgba(0, 240, 255, 0.18) 0%, rgba(0, 240, 255, 0) 46%),
|
||
radial-gradient(circle at 82% 70%, rgba(165, 90, 255, 0.14) 0%, rgba(165, 90, 255, 0) 52%);
|
||
background-size:
|
||
34rpx 2rpx,
|
||
2rpx 34rpx,
|
||
cover,
|
||
cover,
|
||
cover;
|
||
background-position:
|
||
14rpx 12rpx,
|
||
12rpx 14rpx,
|
||
calc(100% - 14rpx) 12rpx,
|
||
calc(100% - 12rpx) 14rpx,
|
||
14rpx calc(100% - 12rpx),
|
||
12rpx calc(100% - 14rpx),
|
||
calc(100% - 14rpx) calc(100% - 12rpx),
|
||
calc(100% - 12rpx) calc(100% - 14rpx);
|
||
background-repeat: no-repeat;
|
||
opacity: 0.65;
|
||
}
|
||
.page.big:after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 14rpx;
|
||
right: 14rpx;
|
||
bottom: 0;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, rgba(0, 240, 255, 0) 0%, rgba(0, 240, 255, 0.45) 50%, rgba(0, 240, 255, 0) 100%);
|
||
opacity: 0.75;
|
||
}
|
||
.big-top {
|
||
height: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
padding: 12rpx 0;
|
||
}
|
||
.big-title {
|
||
position: relative;
|
||
z-index: 3;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: var(--font-3xl);
|
||
font-weight: 900;
|
||
letter-spacing: 2rpx;
|
||
color: rgba(242, 252, 255, 0.98);
|
||
min-height: calc(120rpx + 1.2vh);
|
||
width: 100%;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
text-shadow:
|
||
0 2rpx 12rpx rgba(0, 0, 0, 0.65),
|
||
0 0 18rpx rgba(0, 240, 255, 0.26),
|
||
0 0 32rpx rgba(60, 140, 255, 0.18);
|
||
}
|
||
.big-title:before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image: url('/static/title.png');
|
||
background-size: 100% 100%;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
pointer-events: none;
|
||
z-index: 2;
|
||
}
|
||
.big-title-text {
|
||
position: relative;
|
||
z-index: 3;
|
||
padding: 0 40rpx;
|
||
box-sizing: border-box;
|
||
font-size: calc(48rpx + 0.9vw);
|
||
}
|
||
.big-title {
|
||
min-height: calc(120rpx + 1.2vh);
|
||
width: 100%;
|
||
padding: 0;
|
||
}
|
||
.big-title i {
|
||
display: none;
|
||
}
|
||
.big-top-actions {
|
||
position: relative;
|
||
z-index: 1;
|
||
margin-top: 12rpx;
|
||
border-radius: 14rpx;
|
||
border: 1px solid rgba(0, 240, 255, 0.18);
|
||
background: linear-gradient(180deg, rgba(2, 10, 26, 0.88) 0%, rgba(2, 10, 26, 0.70) 100%);
|
||
box-shadow: 0 10rpx 22rpx rgba(0, 0, 0, 0.40);
|
||
backdrop-filter: blur(10px);
|
||
overflow: hidden;
|
||
padding: 10rpx 10rpx;
|
||
}
|
||
.big-top-action-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||
gap: 12rpx;
|
||
align-items: center;
|
||
}
|
||
.big-top-action-item {
|
||
height: calc(140rpx + 1.5vh);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 14rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.16);
|
||
background: rgba(7, 13, 28, 0.38);
|
||
box-sizing: border-box;
|
||
}
|
||
.big-top-action-ico {
|
||
width: calc(124rpx + 1.2vh);
|
||
height: calc(124rpx + 1.2vh);
|
||
filter: drop-shadow(0 0 14rpx rgba(0, 240, 255, 0.26)) drop-shadow(0 0 26rpx rgba(60, 140, 255, 0.18));
|
||
}
|
||
.big-grid {
|
||
margin-top: 14rpx;
|
||
display: flex;
|
||
gap: 12rpx;
|
||
width: 100%;
|
||
max-width: none;
|
||
margin-left: 0;
|
||
margin-right: 0;
|
||
justify-content: space-between;
|
||
}
|
||
.big-col {
|
||
width: 27%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
.big-center {
|
||
flex: 0 0 46%;
|
||
width: 46%;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
.big-panel {
|
||
position: relative;
|
||
border-radius: 18rpx;
|
||
padding: 18rpx 18rpx 16rpx;
|
||
min-height: calc(320rpx + 2vh);
|
||
border: 1px solid rgba(0, 240, 255, 0.16);
|
||
background-image:
|
||
linear-gradient(135deg, rgba(0, 240, 255, 0.08) 0%, rgba(60, 140, 255, 0.05) 35%, rgba(165, 90, 255, 0.06) 100%),
|
||
linear-gradient(180deg, rgba(7, 13, 28, 0.72) 0%, rgba(7, 13, 28, 0.56) 100%);
|
||
background-repeat: no-repeat;
|
||
background-size: cover;
|
||
backdrop-filter: blur(6px);
|
||
box-shadow: 0 14rpx 26rpx rgba(0, 0, 0, 0.42);
|
||
overflow: hidden;
|
||
}
|
||
.big-panel-core {
|
||
min-height: calc(460rpx + 3vh);
|
||
}
|
||
.big-panel-core .big-ring {
|
||
height: calc(440rpx + 3vh);
|
||
}
|
||
.big-panel-core .big-ring:before {
|
||
width: calc(440rpx + 3vh);
|
||
height: calc(440rpx + 3vh);
|
||
}
|
||
.big-panel-core .big-ring-center {
|
||
width: calc(250rpx + 2vh);
|
||
height: calc(250rpx + 2vh);
|
||
}
|
||
.big-metrics {
|
||
position: relative;
|
||
z-index: 3;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: center;
|
||
padding: 8rpx 0 16rpx;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
.big-metric {
|
||
text-align: center;
|
||
min-width: 340rpx;
|
||
}
|
||
.big-metric-label {
|
||
font-size: var(--font-sm);
|
||
font-weight: 700;
|
||
color: rgba(220, 250, 255, 0.78);
|
||
letter-spacing: 1rpx;
|
||
margin-bottom: 6rpx;
|
||
}
|
||
.big-metric-value {
|
||
display: block;
|
||
font-size: var(--font-2xl);
|
||
font-weight: 900;
|
||
line-height: 1;
|
||
color: rgba(242, 252, 255, 0.96);
|
||
text-shadow:
|
||
0 10rpx 28rpx rgba(0, 0, 0, 0.55),
|
||
0 0 18rpx rgba(0, 240, 255, 0.22);
|
||
}
|
||
.big-panel-core .big-ring {
|
||
margin-top: 4rpx;
|
||
}
|
||
.big-panel:before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
background-image:
|
||
linear-gradient(90deg, rgba(0,240,255,0.65), rgba(0,240,255,0)),
|
||
linear-gradient(180deg, rgba(0,240,255,0.65), rgba(0,240,255,0)),
|
||
linear-gradient(270deg, rgba(0,240,255,0.65), rgba(0,240,255,0)),
|
||
linear-gradient(180deg, rgba(0,240,255,0.65), rgba(0,240,255,0)),
|
||
linear-gradient(90deg, rgba(60,140,255,0.35), rgba(60,140,255,0)),
|
||
linear-gradient(0deg, rgba(60,140,255,0.35), rgba(60,140,255,0)),
|
||
linear-gradient(270deg, rgba(60,140,255,0.35), rgba(60,140,255,0)),
|
||
linear-gradient(0deg, rgba(60,140,255,0.35), rgba(60,140,255,0));
|
||
background-size:
|
||
34rpx 2rpx,
|
||
2rpx 34rpx,
|
||
34rpx 2rpx,
|
||
2rpx 34rpx,
|
||
34rpx 2rpx,
|
||
2rpx 34rpx,
|
||
34rpx 2rpx,
|
||
2rpx 34rpx;
|
||
background-position:
|
||
14rpx 12rpx,
|
||
12rpx 14rpx,
|
||
calc(100% - 14rpx) 12rpx,
|
||
calc(100% - 12rpx) 14rpx,
|
||
14rpx calc(100% - 12rpx),
|
||
12rpx calc(100% - 14rpx),
|
||
calc(100% - 14rpx) calc(100% - 12rpx),
|
||
calc(100% - 12rpx) calc(100% - 14rpx);
|
||
background-repeat: no-repeat;
|
||
opacity: 0.65;
|
||
}
|
||
.big-panel:after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 14rpx;
|
||
right: 14rpx;
|
||
bottom: 0;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, rgba(0,240,255,0) 0%, rgba(0,240,255,0.45) 50%, rgba(0,240,255,0) 100%);
|
||
opacity: 0.75;
|
||
}
|
||
.big-panel-head {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
justify-content: space-between;
|
||
margin-bottom: 14rpx;
|
||
}
|
||
.big-panel-head:after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: -10rpx;
|
||
height: 1px;
|
||
background: linear-gradient(90deg, rgba(0,240,255,0) 0%, rgba(0,240,255,0.22) 18%, rgba(0,240,255,0.55) 50%, rgba(0,240,255,0.22) 82%, rgba(0,240,255,0) 100%);
|
||
opacity: 0.95;
|
||
}
|
||
.big-panel-title {
|
||
font-size: var(--font-lg);
|
||
font-weight: 900;
|
||
color: rgba(242, 252, 255, 0.94);
|
||
padding-left: 14rpx;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
.big-panel-report .big-panel-head {
|
||
margin-bottom: 18rpx;
|
||
padding: 8rpx 14rpx 12rpx;
|
||
border-radius: 14rpx;
|
||
background: linear-gradient(90deg, rgba(0, 240, 255, 0.12) 0%, rgba(2, 8, 22, 0.18) 45%, rgba(165, 90, 255, 0.10) 100%);
|
||
border: 1px solid rgba(0, 240, 255, 0.18);
|
||
box-shadow: 0 0 18rpx rgba(0, 240, 255, 0.10);
|
||
overflow: hidden;
|
||
}
|
||
.big-panel-report .big-panel-head:after {
|
||
bottom: 0;
|
||
height: 2rpx;
|
||
background: linear-gradient(90deg, rgba(0,240,255,0) 0%, rgba(0,240,255,0.65) 20%, rgba(165,90,255,0.55) 60%, rgba(0,240,255,0) 100%);
|
||
opacity: 1;
|
||
}
|
||
.big-panel-report .big-panel-head:before {
|
||
content: '';
|
||
position: absolute;
|
||
right: -40rpx;
|
||
top: -50rpx;
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
background: radial-gradient(circle at 30% 30%, rgba(0, 240, 255, 0.22) 0%, rgba(0, 240, 255, 0) 62%);
|
||
transform: rotate(12deg);
|
||
pointer-events: none;
|
||
opacity: 0.9;
|
||
}
|
||
.big-panel-report .big-panel-title {
|
||
padding-left: 20rpx;
|
||
text-shadow:
|
||
0 0 16rpx rgba(0, 240, 255, 0.22),
|
||
0 2rpx 10rpx rgba(0, 0, 0, 0.55);
|
||
}
|
||
.big-panel-report .big-panel-title:before {
|
||
left: 10rpx;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
height: calc(22rpx + 0.8vh);
|
||
background: linear-gradient(180deg, rgba(0, 240, 255, 0.95) 0%, rgba(39, 120, 255, 0.85) 55%, rgba(165, 90, 255, 0.85) 100%);
|
||
box-shadow: 0 0 14rpx rgba(0, 240, 255, 0.18);
|
||
}
|
||
.big-panel-portrait {
|
||
border-color: transparent;
|
||
box-shadow: none;
|
||
}
|
||
.big-panel-portrait:before,
|
||
.big-panel-portrait:after {
|
||
opacity: 0;
|
||
}
|
||
.big-panel-portrait .big-panel-head:after {
|
||
opacity: 0;
|
||
}
|
||
.big-panel-portrait .big-panel-title:before {
|
||
opacity: 0;
|
||
}
|
||
.big-panel-title:before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 6rpx;
|
||
height: calc(24rpx + 0.8vh);
|
||
border-radius: 6rpx;
|
||
background: linear-gradient(180deg, rgba(116, 216, 255, 0.95) 0%, rgba(43, 107, 255, 0.85) 100%);
|
||
}
|
||
.big-panel-sub {
|
||
font-size: var(--font-sm);
|
||
color: rgba(242, 252, 255, 0.84);
|
||
font-weight: 800;
|
||
}
|
||
.big-ai-head-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14rpx;
|
||
}
|
||
.big-panel-ai.is-collapsed {
|
||
padding-bottom: 10rpx;
|
||
}
|
||
.big-panel-ai.is-open {
|
||
padding-bottom: 22rpx;
|
||
}
|
||
.big-panel-clear {
|
||
padding: 6rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
font-size: var(--font-xs);
|
||
font-weight: 900;
|
||
color: rgba(242, 252, 255, 0.86);
|
||
border: 1px solid rgba(116, 216, 255, 0.20);
|
||
background: rgba(7, 13, 28, 0.45);
|
||
box-sizing: border-box;
|
||
}
|
||
.big-panel-clear:active {
|
||
opacity: 0.85;
|
||
}
|
||
.big-panel-ai {
|
||
margin-top: 14rpx;
|
||
}
|
||
.big-panel-head-ai {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
}
|
||
.big-ai-body {
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
.big-ai-messages {
|
||
height: calc(640rpx + 4vh);
|
||
padding: 8rpx 10rpx;
|
||
box-sizing: border-box;
|
||
border-radius: 14rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.14);
|
||
background: rgba(7, 13, 28, 0.55);
|
||
}
|
||
.big-ai-empty {
|
||
padding: 16rpx 10rpx;
|
||
font-size: var(--font-xs);
|
||
color: rgba(242, 252, 255, 0.62);
|
||
font-weight: 700;
|
||
line-height: 1.4;
|
||
}
|
||
.big-ai-msg {
|
||
display: flex;
|
||
margin: 10rpx 0;
|
||
}
|
||
.big-ai-msg.is-user {
|
||
justify-content: flex-end;
|
||
}
|
||
.big-ai-msg.is-ai {
|
||
justify-content: flex-start;
|
||
}
|
||
.big-ai-bubble {
|
||
max-width: 78%;
|
||
padding: 12rpx 14rpx;
|
||
border-radius: 14rpx;
|
||
font-size: var(--font-sm);
|
||
line-height: 1.45;
|
||
color: rgba(242, 252, 255, 0.92);
|
||
border: 1px solid rgba(0, 240, 255, 0.16);
|
||
background: linear-gradient(180deg, rgba(0, 188, 255, 0.10) 0%, rgba(2, 8, 22, 0.82) 70%, rgba(2, 8, 22, 0.92) 100%);
|
||
box-shadow: 0 0 16rpx rgba(0, 166, 255, 0.08);
|
||
word-break: break-all;
|
||
}
|
||
.big-ai-msg.is-user .big-ai-bubble {
|
||
border-color: rgba(43, 107, 255, 0.22);
|
||
background: linear-gradient(180deg, rgba(43, 107, 255, 0.16) 0%, rgba(2, 8, 22, 0.82) 70%, rgba(2, 8, 22, 0.92) 100%);
|
||
}
|
||
.big-ai-bottom {
|
||
height: 1px;
|
||
}
|
||
.big-ai-input {
|
||
margin-top: 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
.big-ai-voice-btn {
|
||
width: calc(72rpx + 2vh);
|
||
height: calc(72rpx + 2vh);
|
||
border-radius: 14rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 1px solid rgba(116, 216, 255, 0.18);
|
||
background: rgba(7, 13, 28, 0.78);
|
||
transition: all 0.3s ease;
|
||
}
|
||
.big-ai-voice-btn.active {
|
||
border-color: rgba(0, 240, 255, 0.6);
|
||
background: rgba(0, 240, 255, 0.12);
|
||
box-shadow: 0 0 20rpx rgba(0, 240, 255, 0.4);
|
||
}
|
||
.big-ai-text {
|
||
flex: 1;
|
||
height: calc(72rpx + 2vh);
|
||
border-radius: 14rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.18);
|
||
background: rgba(7, 13, 28, 0.78);
|
||
padding: 0 14rpx;
|
||
font-size: var(--font-sm);
|
||
color: rgba(242, 252, 255, 0.92);
|
||
box-sizing: border-box;
|
||
}
|
||
.big-ai-send {
|
||
width: 120rpx;
|
||
height: calc(72rpx + 2vh);
|
||
border-radius: 14rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: var(--font-sm);
|
||
font-weight: 900;
|
||
color: rgba(242, 252, 255, 0.92);
|
||
border: 1px solid rgba(0, 240, 255, 0.22);
|
||
background: linear-gradient(180deg, rgba(0, 240, 255, 0.16) 0%, rgba(2, 8, 22, 0.92) 100%);
|
||
box-sizing: border-box;
|
||
}
|
||
.big-ai-send.disabled {
|
||
opacity: 0.5;
|
||
}
|
||
.big-ring {
|
||
position: relative;
|
||
z-index: 1;
|
||
height: calc(360rpx + 2.5vh);
|
||
}
|
||
.big-ring:before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: calc(360rpx + 2.5vh);
|
||
height: calc(360rpx + 2.5vh);
|
||
border-radius: 999rpx;
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
background-image: url('/static/hud-ring.png');
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
background-size: contain;
|
||
opacity: 0.38;
|
||
filter: drop-shadow(0 0 18rpx rgba(0, 240, 255, 0.22));
|
||
animation: bigRingSpin 22s linear infinite;
|
||
}
|
||
@keyframes bigRingSpin {
|
||
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
||
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
||
}
|
||
.big-ring-center {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: calc(210rpx + 5vh);
|
||
height: calc(210rpx + 5vh);
|
||
border-radius: 999rpx;
|
||
background: rgba(7, 13, 28, 0.72);
|
||
z-index: 5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
.big-ring-center-main {
|
||
font-size: var(--font-lg);
|
||
font-weight: 900;
|
||
color: rgba(242, 252, 255, 0.96);
|
||
}
|
||
.big-ring-center-sub {
|
||
font-size: var(--font-xs);
|
||
color: rgba(242, 252, 255, 0.84);
|
||
text-align: center;
|
||
line-height: 26rpx;
|
||
padding: 0 12rpx;
|
||
}
|
||
.big-chart {
|
||
position: relative;
|
||
z-index: 1;
|
||
height: calc(340rpx + 2.5vh);
|
||
}
|
||
.big-portrait-cloud {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
.big-portrait-word {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
font-weight: 800;
|
||
letter-spacing: 1rpx;
|
||
white-space: nowrap;
|
||
text-shadow:
|
||
0 2rpx 10rpx rgba(0, 0, 0, 0.55),
|
||
0 0 14rpx rgba(0, 240, 255, 0.08);
|
||
filter: saturate(1.08);
|
||
will-change: transform;
|
||
}
|
||
@keyframes portraitFloat1 {
|
||
0% { transform: translate(-50%, -50%) translate(0, 0); }
|
||
50% { transform: translate(-50%, -50%) translate(0, -10rpx); }
|
||
100% { transform: translate(-50%, -50%) translate(0, 0); }
|
||
}
|
||
@keyframes portraitFloat2 {
|
||
0% { transform: translate(-50%, -50%) translate(0, 0); }
|
||
50% { transform: translate(-50%, -50%) translate(10rpx, 0); }
|
||
100% { transform: translate(-50%, -50%) translate(0, 0); }
|
||
}
|
||
@keyframes portraitFloat3 {
|
||
0% { transform: translate(-50%, -50%) translate(0, 0); }
|
||
50% { transform: translate(-50%, -50%) translate(-8rpx, 8rpx); }
|
||
100% { transform: translate(-50%, -50%) translate(0, 0); }
|
||
}
|
||
@keyframes portraitFloat4 {
|
||
0% { transform: translate(-50%, -50%) translate(0, 0); }
|
||
50% { transform: translate(-50%, -50%) translate(8rpx, -6rpx); }
|
||
100% { transform: translate(-50%, -50%) translate(0, 0); }
|
||
}
|
||
.big-chart-xl {
|
||
height: calc(400rpx + 3vh);
|
||
}
|
||
.big-kpis {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
}
|
||
.big-kpi {
|
||
width: calc(50% - 6rpx);
|
||
border-radius: 14rpx;
|
||
padding: 14rpx 12rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.18);
|
||
background: rgba(7, 13, 28, 0.78);
|
||
box-sizing: border-box;
|
||
}
|
||
.big-kpi-num-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
.big-kpi-ico {
|
||
width: calc(34rpx + 1vh);
|
||
height: calc(34rpx + 1vh);
|
||
filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
|
||
}
|
||
.big-kpi-num {
|
||
font-size: var(--font-xl);
|
||
font-weight: 900;
|
||
color: #74d8ff;
|
||
text-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.65);
|
||
}
|
||
.big-kpi-lab {
|
||
margin-top: 6rpx;
|
||
font-size: var(--font-sm);
|
||
color: rgba(242, 252, 255, 0.84);
|
||
font-weight: 800;
|
||
}
|
||
.big-tools {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
gap: 12rpx;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
.big-action-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 18rpx;
|
||
padding: 10rpx 2rpx 2rpx;
|
||
}
|
||
.big-panel-actions {
|
||
min-height: calc(380rpx + 3vh);
|
||
}
|
||
.big-action-item {
|
||
position: relative;
|
||
z-index: 1;
|
||
height: calc(150rpx + 1.5vh);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 10rpx;
|
||
padding: 14rpx 10rpx;
|
||
border-radius: 16rpx;
|
||
border: 1px solid rgba(0, 188, 255, 0.22);
|
||
background: linear-gradient(180deg, rgba(0, 188, 255, 0.10) 0%, rgba(2, 8, 22, 0.82) 70%, rgba(2, 8, 22, 0.92) 100%);
|
||
box-shadow: 0 0 18rpx rgba(0, 166, 255, 0.10);
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
}
|
||
.big-action-item:active { transform: scale(0.98); }
|
||
.big-action-ico {
|
||
width: calc(68rpx + 2vh);
|
||
height: calc(68rpx + 2vh);
|
||
flex: 0 0 auto;
|
||
filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
|
||
}
|
||
.big-action-text {
|
||
font-size: var(--font-base);
|
||
font-weight: 900;
|
||
color: rgba(242, 252, 255, 0.92);
|
||
line-height: 1.1;
|
||
text-align: center;
|
||
}
|
||
.big-action-item.c-blue { border-color: rgba(0, 188, 255, 0.40); box-shadow: 0 0 20rpx rgba(0, 188, 255, 0.14); }
|
||
.big-action-item.c-blue2 { border-color: rgba(39, 120, 255, 0.40); box-shadow: 0 0 20rpx rgba(39, 120, 255, 0.14); }
|
||
.big-action-item.c-red { border-color: rgba(255, 72, 92, 0.40); box-shadow: 0 0 20rpx rgba(255, 72, 92, 0.12); }
|
||
.big-action-item.c-cyan { border-color: rgba(0, 240, 255, 0.40); box-shadow: 0 0 20rpx rgba(0, 240, 255, 0.12); }
|
||
.big-action-item.c-yellow { border-color: rgba(255, 196, 45, 0.40); box-shadow: 0 0 20rpx rgba(255, 196, 45, 0.12); }
|
||
.big-action-item.c-purple { border-color: rgba(165, 90, 255, 0.40); box-shadow: 0 0 20rpx rgba(165, 90, 255, 0.12); }
|
||
.big-tool {
|
||
flex: 1;
|
||
border-radius: 14rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.18);
|
||
background: rgba(7, 13, 28, 0.78);
|
||
padding: 12rpx 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
.big-tool-icon {
|
||
width: calc(50rpx + 1.5vh);
|
||
height: calc(50rpx + 1.5vh);
|
||
border-radius: 12rpx;
|
||
border: 1px solid rgba(116, 216, 255, 0.22);
|
||
background: rgba(10, 18, 38, 0.55);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.big-tool-text {
|
||
font-size: var(--font-sm);
|
||
font-weight: 900;
|
||
color: rgba(242, 252, 255, 0.92);
|
||
}
|
||
.big-nav {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12rpx;
|
||
}
|
||
.big-nav-item {
|
||
width: calc(33.33% - 8rpx);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
padding: 10rpx 0;
|
||
}
|
||
.big-hex {
|
||
width: calc(96rpx + 3vh);
|
||
height: calc(86rpx + 2.7vh);
|
||
clip-path: polygon(25% 6.7%, 75% 6.7%, 100% 50%, 75% 93.3%, 25% 93.3%, 0% 50%);
|
||
border: 1px solid rgba(116, 216, 255, 0.28);
|
||
background: radial-gradient(circle at 50% 30%, rgba(116, 216, 255, 0.22) 0%, rgba(10, 18, 38, 0.55) 70%);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 0 18rpx rgba(116, 216, 255, 0.18);
|
||
}
|
||
.big-nav-text {
|
||
font-size: var(--font-xs);
|
||
font-weight: 900;
|
||
color: rgba(242, 252, 255, 0.86);
|
||
}
|
||
.hex-img { display: none; }
|
||
|
||
@media (max-width: 980px) {
|
||
.big-grid {
|
||
flex-direction: column;
|
||
}
|
||
.big-col {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
/* 登录弹窗显示时禁用页面交互 */
|
||
.page-disabled {
|
||
pointer-events: none;
|
||
user-select: none;
|
||
}
|
||
</style>
|