xinli/xinlidsj/pages/index/index.vue
2026-02-24 16:49:05 +08:00

2671 lines
83 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">
<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>
</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">
<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>
</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 { 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'
export default {
components: {
QiunDataCharts,
UniIcons
},
data() {
return {
socketOpen: false,
connecting: false,
voiceTipsOpen: false,
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
}
if (this.isH5) {
this.fetchCenterVideo()
}
const token = getToken()
if (!token) return
this.initMessageChannel()
this.checkUnreadNoticePopup()
this.checkUnreadMessagePopup()
if (this.isH5) {
this.fetchBigData()
this.fetchInboxList()
}
},
onShow() {
const token = getToken()
if (!token) return
this.checkUnreadNoticePopup()
this.checkUnreadMessagePopup()
if (this.isH5) {
this.fetchInboxList()
}
},
methods: {
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()
})
}
},
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'
})
},
goSettings() {
uni.switchTab({
url: '/pages/settings/index'
})
}
}
}
</script>
<style scoped>
.page {
min-height: 100vh;
padding: 24rpx 24rpx 120rpx;
box-sizing: border-box;
background: #F4F6FB;
--c-primary: #1677ff;
--c-danger: #E87A7A;
}
.section {
margin-top: 14rpx;
}
.section-title {
font-size: 28rpx;
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: 64rpx;
height: 64rpx;
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: 32rpx;
font-weight: 700;
color: #111827;
}
.card-desc {
margin-top: 10rpx;
font-size: 24rpx;
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: 24rpx;
font-weight: 700;
color: #334155;
}
.fold-body {
padding: 0 14rpx 14rpx;
}
.fold-item {
font-size: 24rpx;
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: 22rpx;
color: #334155;
}
.card.disabled {
opacity: 0.6;
}
.page.big {
min-height: 100vh;
padding: 18rpx 18rpx 24rpx;
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: 24rpx;
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: 720rpx;
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: 26rpx;
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: 22rpx;
line-height: 32rpx;
color: rgba(201, 242, 255, 0.72);
}
.big-panel-inbox {
margin-top: 16rpx;
min-height: 320rpx;
}
.big-panel-portrait .big-chart {
height: 420rpx;
}
.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: 24rpx;
font-weight: 900;
color: rgba(242, 252, 255, 0.92);
}
.big-inbox-desc {
margin-top: 8rpx;
font-size: 22rpx;
color: rgba(242, 252, 255, 0.84);
line-height: 28rpx;
max-height: 56rpx;
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: 60rpx;
font-weight: 900;
letter-spacing: 2rpx;
color: rgba(242, 252, 255, 0.98);
min-height: 120rpx;
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;
}
.big-title {
min-height: 120rpx;
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(5, minmax(0, 1fr));
gap: 12rpx;
align-items: center;
}
.big-top-action-item {
height: 140rpx;
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: 124rpx;
height: 124rpx;
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: 22rpx;
width: 100%;
max-width: none;
margin-left: 0;
margin-right: 0;
justify-content: space-between;
}
.big-col {
width: 28%;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.big-center {
flex: 0 0 44%;
width: 44%;
min-width: 0;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.big-panel {
position: relative;
border-radius: 18rpx;
padding: 26rpx 26rpx 22rpx;
min-height: 320rpx;
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: 460rpx;
}
.big-panel-core .big-ring {
height: 440rpx;
}
.big-panel-core .big-ring:before {
width: 440rpx;
height: 440rpx;
}
.big-panel-core .big-ring-center {
width: 250rpx;
height: 250rpx;
}
.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: 24rpx;
font-weight: 700;
color: rgba(220, 250, 255, 0.78);
letter-spacing: 1rpx;
margin-bottom: 6rpx;
}
.big-metric-value {
display: block;
font-size: 56rpx;
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: 32rpx;
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: 10rpx;
height: 22rpx;
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: 6rpx;
width: 6rpx;
height: 24rpx;
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: 24rpx;
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: 22rpx;
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: 640rpx;
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: 22rpx;
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: 24rpx;
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-text {
flex: 1;
height: 72rpx;
border-radius: 14rpx;
border: 1px solid rgba(116, 216, 255, 0.18);
background: rgba(7, 13, 28, 0.78);
padding: 0 14rpx;
font-size: 24rpx;
color: rgba(242, 252, 255, 0.92);
box-sizing: border-box;
}
.big-ai-send {
width: 120rpx;
height: 72rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
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: 360rpx;
}
.big-ring:before {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 360rpx;
height: 360rpx;
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: 210rpx;
height: 210rpx;
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: 34rpx;
font-weight: 900;
color: rgba(242, 252, 255, 0.96);
}
.big-ring-center-sub {
font-size: 22rpx;
color: rgba(242, 252, 255, 0.84);
text-align: center;
line-height: 26rpx;
padding: 0 12rpx;
}
.big-chart {
position: relative;
z-index: 1;
height: 340rpx;
}
.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: 400rpx;
}
.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: 34rpx;
height: 34rpx;
filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
}
.big-kpi-num {
font-size: 38rpx;
font-weight: 900;
color: #74d8ff;
text-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.65);
}
.big-kpi-lab {
margin-top: 6rpx;
font-size: 24rpx;
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: 380rpx;
}
.big-action-item {
position: relative;
z-index: 1;
height: 150rpx;
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: 68rpx;
height: 68rpx;
flex: 0 0 auto;
filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
}
.big-action-text {
font-size: 26rpx;
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: 50rpx;
height: 50rpx;
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: 24rpx;
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: 96rpx;
height: 86rpx;
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: 22rpx;
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%;
}
}
</style>