xinli/xinlidsj/pages/index/index.vue

3475 lines
108 KiB
Vue
Raw Normal View History

<template>
2026-02-25 18:16:20 +08:00
<!-- 主内容区域 - 登录时禁用交互 -->
<view class="page" v-if="!isH5" :class="{ 'page-disabled': showLoginModal }">
2026-02-24 16:49:05 +08:00
<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>
2026-02-25 18:16:20 +08:00
<view class="big-top-action-item" @tap="goNotice">
<image class="big-top-action-ico" src="/static/7.png" mode="aspectFit" />
</view>
2026-02-24 16:49:05 +08:00
</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" />
2026-02-24 16:49:05 +08:00
</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" />
2026-02-24 16:49:05 +08:00
<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">
2026-02-25 18:16:20 +08:00
<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>
2026-02-24 16:49:05 +08:00
<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" />
2026-02-24 16:49:05 +08:00
</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" />
2026-02-24 16:49:05 +08:00
</view>
</view>
</view>
</view>
</view>
2026-02-25 18:16:20 +08:00
<!-- 登录弹窗 - 放在最后确保在最上层 -->
<LoginModal :show="showLoginModal" :closable="false" @success="onLoginSuccess" @close="showLoginModal = false" />
2026-02-24 16:49:05 +08:00
</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'
2026-02-25 18:16:20 +08:00
import { getBaseUrl } from '../../utils/config'
2026-02-24 16:49:05 +08:00
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'
2026-02-25 18:16:20 +08:00
import LoginModal from '../../components/LoginModal.vue'
2026-02-24 16:49:05 +08:00
export default {
components: {
QiunDataCharts,
2026-02-25 18:16:20 +08:00
UniIcons,
LoginModal
2026-02-24 16:49:05 +08:00
},
// 捕获子组件错误,防止图表错误影响整个应用
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
},
2026-02-24 16:49:05 +08:00
data() {
return {
2026-02-25 18:16:20 +08:00
showLoginModal: false,
hasCheckedLogin: false,
isMounted: false,
chartKey: 0,
resizeTimer: null,
2026-02-24 16:49:05 +08:00
socketOpen: false,
connecting: false,
voiceTipsOpen: false,
2026-02-25 18:16:20 +08:00
voiceMode: false,
recorder: null,
mediaRecorder: null,
mediaStream: null,
audioChunks: [],
2026-02-24 16:49:05 +08:00
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 = ''
})
2026-02-24 16:49:05 +08:00
try {
const info = uni.getSystemInfoSync()
console.log('系统信息:', info)
2026-02-24 16:49:05 +08:00
this.isH5 = info && info.uniPlatform === 'web'
console.log('isH5:', this.isH5)
2026-02-24 16:49:05 +08:00
} catch (e) {
console.error('获取系统信息失败:', e)
2026-02-24 16:49:05 +08:00
this.isH5 = false
}
2026-02-25 18:16:20 +08:00
// 检查登录状态
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)
2026-02-24 16:49:05 +08:00
}
},
onShow() {
console.log('=== onShow 触发 ===')
2026-02-25 18:16:20 +08:00
// 检查登录状态
this.checkLoginStatus()
2026-02-24 16:49:05 +08:00
},
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
},
2026-02-25 18:16:20 +08:00
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()
2026-02-25 18:16:20 +08:00
},
2026-02-24 16:49:05 +08:00
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(/&nbsp;/gi, ' ')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&amp;/gi, '&')
.replace(/&quot;/gi, '"')
.replace(/&#39;/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()
})
}
},
2026-02-25 18:16:20 +08:00
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'
})
})
},
2026-02-24 16:49:05 +08:00
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)
},
2026-02-26 18:18:03 +08:00
// 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
}
},
2026-02-24 16:49:05 +08:00
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*的?(?:报告|数据|画像|档案|信息)/)
2026-02-24 16:49:05 +08:00
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/分析张三的画像”等平台内分析指令'
2026-02-24 16:49:05 +08:00
},
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
},
2026-02-26 18:18:03 +08:00
// 执行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() {
2026-02-24 16:49:05 +08:00
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()
})
2026-02-26 18:18:03 +08:00
// ===== 新增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意图识别结束 =====
2026-02-24 16:49:05 +08:00
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
}
2026-02-26 18:18:03 +08:00
// 优先使用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)
2026-02-24 16:49:05 +08:00
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存在')
2026-02-24 16:49:05 +08:00
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)
2026-02-24 16:49:05 +08:00
const aData = aRes && aRes.data ? aRes.data : null
const dData = dRes && dRes.data ? dRes.data : null
// 认证失败已在request.js中统一处理这里只处理其他错误
2026-02-24 16:49:05 +08:00
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
}
2026-02-24 16:49:05 +08:00
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)
2026-02-24 16:49:05 +08:00
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
}
2026-02-24 16:49:05 +08:00
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'
})
},
2026-02-25 18:16:20 +08:00
goNotice() {
uni.navigateTo({
url: '/pages/message/notice'
})
},
2026-02-24 16:49:05 +08:00
goSettings() {
2026-02-25 18:16:20 +08:00
uni.navigateTo({
2026-02-24 16:49:05 +08:00
url: '/pages/settings/index'
})
}
}
}
</script>
<style scoped>
2026-02-25 18:16:20 +08:00
/* 自适应字体大小变量 - 基于视口宽度 */
: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); /* 巨大字体 */
}
2026-02-24 16:49:05 +08:00
.page {
min-height: 100vh;
2026-02-25 18:16:20 +08:00
padding: 24rpx 24rpx 0;
2026-02-24 16:49:05 +08:00
box-sizing: border-box;
background: #F4F6FB;
--c-primary: #1677ff;
--c-danger: #E87A7A;
2026-02-25 18:16:20 +08:00
/* 页面级字体变量 */
--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);
2026-02-24 16:49:05 +08:00
}
.section {
margin-top: 14rpx;
}
.section-title {
2026-02-25 18:16:20 +08:00
font-size: var(--font-md);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
width: calc(64rpx + 1.5vh);
height: calc(64rpx + 1.5vh);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
font-size: var(--font-lg);
2026-02-24 16:49:05 +08:00
font-weight: 700;
color: #111827;
}
.card-desc {
margin-top: 10rpx;
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
font-weight: 700;
color: #334155;
}
.fold-body {
padding: 0 14rpx 14rpx;
}
.fold-item {
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
font-size: var(--font-xs);
2026-02-24 16:49:05 +08:00
color: #334155;
}
.card.disabled {
opacity: 0.6;
}
.page.big {
min-height: 100vh;
padding: 12rpx 12rpx 20rpx;
2026-02-24 16:49:05 +08:00
box-sizing: border-box;
position: relative;
overflow-x: hidden;
overflow-y: auto;
2026-02-24 16:49:05 +08:00
--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);
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
line-height: 1.5;
text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.55);
}
.big-center-media {
width: 100%;
height: calc(74vh - 360rpx);
2026-02-25 18:16:20 +08:00
max-height: calc(720rpx + 7vh);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
font-size: var(--font-base);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
font-size: var(--font-xs);
2026-02-24 16:49:05 +08:00
line-height: 32rpx;
color: rgba(201, 242, 255, 0.72);
}
.big-panel-inbox {
margin-top: 16rpx;
2026-02-25 18:16:20 +08:00
min-height: calc(320rpx + 2vh);
2026-02-24 16:49:05 +08:00
}
.big-panel-portrait .big-chart {
2026-02-25 18:16:20 +08:00
height: calc(420rpx + 3vh);
2026-02-24 16:49:05 +08:00
}
.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 {
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
font-weight: 900;
color: rgba(242, 252, 255, 0.92);
}
.big-inbox-desc {
margin-top: 8rpx;
2026-02-25 18:16:20 +08:00
font-size: var(--font-xs);
2026-02-24 16:49:05 +08:00
color: rgba(242, 252, 255, 0.84);
line-height: 28rpx;
2026-02-25 18:16:20 +08:00
max-height: calc(56rpx + 0.5vh);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
font-size: var(--font-3xl);
2026-02-24 16:49:05 +08:00
font-weight: 900;
letter-spacing: 2rpx;
color: rgba(242, 252, 255, 0.98);
2026-02-25 18:16:20 +08:00
min-height: calc(120rpx + 1.2vh);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
font-size: calc(48rpx + 0.9vw);
2026-02-24 16:49:05 +08:00
}
.big-title {
2026-02-25 18:16:20 +08:00
min-height: calc(120rpx + 1.2vh);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
grid-template-columns: repeat(6, minmax(0, 1fr));
2026-02-24 16:49:05 +08:00
gap: 12rpx;
align-items: center;
}
.big-top-action-item {
2026-02-25 18:16:20 +08:00
height: calc(140rpx + 1.5vh);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
width: calc(124rpx + 1.2vh);
height: calc(124rpx + 1.2vh);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
gap: 12rpx;
2026-02-24 16:49:05 +08:00
width: 100%;
max-width: none;
margin-left: 0;
margin-right: 0;
justify-content: space-between;
}
.big-col {
2026-02-25 18:16:20 +08:00
width: 27%;
2026-02-24 16:49:05 +08:00
display: flex;
flex-direction: column;
gap: 16rpx;
}
.big-center {
2026-02-25 18:16:20 +08:00
flex: 0 0 46%;
width: 46%;
2026-02-24 16:49:05 +08:00
min-width: 0;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.big-panel {
position: relative;
border-radius: 18rpx;
2026-02-25 18:16:20 +08:00
padding: 18rpx 18rpx 16rpx;
min-height: calc(320rpx + 2vh);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
min-height: calc(460rpx + 3vh);
2026-02-24 16:49:05 +08:00
}
.big-panel-core .big-ring {
2026-02-25 18:16:20 +08:00
height: calc(440rpx + 3vh);
2026-02-24 16:49:05 +08:00
}
.big-panel-core .big-ring:before {
2026-02-25 18:16:20 +08:00
width: calc(440rpx + 3vh);
height: calc(440rpx + 3vh);
2026-02-24 16:49:05 +08:00
}
.big-panel-core .big-ring-center {
2026-02-25 18:16:20 +08:00
width: calc(250rpx + 2vh);
height: calc(250rpx + 2vh);
2026-02-24 16:49:05 +08:00
}
.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 {
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
font-weight: 700;
color: rgba(220, 250, 255, 0.78);
letter-spacing: 1rpx;
margin-bottom: 6rpx;
}
.big-metric-value {
display: block;
2026-02-25 18:16:20 +08:00
font-size: var(--font-2xl);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
font-size: var(--font-lg);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
top: 50%;
transform: translateY(-50%);
height: calc(22rpx + 0.8vh);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
top: 50%;
transform: translateY(-50%);
2026-02-24 16:49:05 +08:00
width: 6rpx;
2026-02-25 18:16:20 +08:00
height: calc(24rpx + 0.8vh);
2026-02-24 16:49:05 +08:00
border-radius: 6rpx;
background: linear-gradient(180deg, rgba(116, 216, 255, 0.95) 0%, rgba(43, 107, 255, 0.85) 100%);
}
.big-panel-sub {
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
font-size: var(--font-xs);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
height: calc(640rpx + 4vh);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
font-size: var(--font-xs);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
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;
}
2026-02-25 18:16:20 +08:00
.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);
}
2026-02-24 16:49:05 +08:00
.big-ai-text {
flex: 1;
2026-02-25 18:16:20 +08:00
height: calc(72rpx + 2vh);
2026-02-24 16:49:05 +08:00
border-radius: 14rpx;
border: 1px solid rgba(116, 216, 255, 0.18);
background: rgba(7, 13, 28, 0.78);
padding: 0 14rpx;
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
color: rgba(242, 252, 255, 0.92);
box-sizing: border-box;
}
.big-ai-send {
width: 120rpx;
2026-02-25 18:16:20 +08:00
height: calc(72rpx + 2vh);
2026-02-24 16:49:05 +08:00
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
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;
2026-02-25 18:16:20 +08:00
height: calc(360rpx + 2.5vh);
2026-02-24 16:49:05 +08:00
}
.big-ring:before {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
2026-02-25 18:16:20 +08:00
width: calc(360rpx + 2.5vh);
height: calc(360rpx + 2.5vh);
2026-02-24 16:49:05 +08:00
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%);
2026-02-25 18:16:20 +08:00
width: calc(210rpx + 5vh);
height: calc(210rpx + 5vh);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
font-size: var(--font-lg);
2026-02-24 16:49:05 +08:00
font-weight: 900;
color: rgba(242, 252, 255, 0.96);
}
.big-ring-center-sub {
2026-02-25 18:16:20 +08:00
font-size: var(--font-xs);
2026-02-24 16:49:05 +08:00
color: rgba(242, 252, 255, 0.84);
text-align: center;
line-height: 26rpx;
padding: 0 12rpx;
}
.big-chart {
position: relative;
z-index: 1;
2026-02-25 18:16:20 +08:00
height: calc(340rpx + 2.5vh);
2026-02-24 16:49:05 +08:00
}
.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 {
2026-02-25 18:16:20 +08:00
height: calc(400rpx + 3vh);
2026-02-24 16:49:05 +08:00
}
.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 {
2026-02-25 18:16:20 +08:00
width: calc(34rpx + 1vh);
height: calc(34rpx + 1vh);
2026-02-24 16:49:05 +08:00
filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
}
.big-kpi-num {
2026-02-25 18:16:20 +08:00
font-size: var(--font-xl);
2026-02-24 16:49:05 +08:00
font-weight: 900;
color: #74d8ff;
text-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.65);
}
.big-kpi-lab {
margin-top: 6rpx;
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
min-height: calc(380rpx + 3vh);
2026-02-24 16:49:05 +08:00
}
.big-action-item {
position: relative;
z-index: 1;
2026-02-25 18:16:20 +08:00
height: calc(150rpx + 1.5vh);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
width: calc(68rpx + 2vh);
height: calc(68rpx + 2vh);
2026-02-24 16:49:05 +08:00
flex: 0 0 auto;
filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
}
.big-action-text {
2026-02-25 18:16:20 +08:00
font-size: var(--font-base);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
width: calc(50rpx + 1.5vh);
height: calc(50rpx + 1.5vh);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
font-size: var(--font-sm);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
width: calc(96rpx + 3vh);
height: calc(86rpx + 2.7vh);
2026-02-24 16:49:05 +08:00
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 {
2026-02-25 18:16:20 +08:00
font-size: var(--font-xs);
2026-02-24 16:49:05 +08:00
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%;
}
}
2026-02-25 18:16:20 +08:00
/* 登录弹窗显示时禁用页面交互 */
.page-disabled {
pointer-events: none;
user-select: none;
}
2026-02-24 16:49:05 +08:00
</style>