xinli/xinlidsj/pages/index/index.vue
2026-02-25 18:16:20 +08:00

3009 lines
92 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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

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