2671 lines
83 KiB
Vue
2671 lines
83 KiB
Vue
|
|
<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(/ /gi, ' ')
|
|||
|
|
.replace(/</gi, '<')
|
|||
|
|
.replace(/>/gi, '>')
|
|||
|
|
.replace(/&/gi, '&')
|
|||
|
|
.replace(/"/gi, '"')
|
|||
|
|
.replace(/'/gi, "'")
|
|||
|
|
s = s.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|||
|
|
s = s.replace(/[ \t\f\v]+/g, ' ')
|
|||
|
|
s = s.replace(/\n\s*\n\s*\n+/g, '\n\n')
|
|||
|
|
return s.trim()
|
|||
|
|
}
|
|||
|
|
const compressMessage = (text, maxLen = 3200) => {
|
|||
|
|
const s = String(text || '')
|
|||
|
|
if (!s) return ''
|
|||
|
|
if (s.length <= maxLen) return s
|
|||
|
|
const headLen = Math.max(600, Math.floor(maxLen / 2) - 200)
|
|||
|
|
const tailLen = maxLen - headLen
|
|||
|
|
const head = s.slice(0, headLen)
|
|||
|
|
const tail = s.slice(-tailLen)
|
|||
|
|
return head + '\n...\n' + tail
|
|||
|
|
}
|
|||
|
|
const parseContent = (data) => {
|
|||
|
|
if (typeof data === 'string') return data
|
|||
|
|
if (!data || typeof data !== 'object') return ''
|
|||
|
|
// 兼容后端 AjaxResult: { code, msg, data?, content? }
|
|||
|
|
if (data && data.code != null && (data.content != null || data.data != null)) {
|
|||
|
|
const c = data.content != null ? data.content : data.data
|
|||
|
|
if (typeof c === 'string') return c
|
|||
|
|
if (c && typeof c === 'object') {
|
|||
|
|
return (
|
|||
|
|
(c && c.content != null ? String(c.content) : '') ||
|
|||
|
|
(c && c.message && c.message.content != null ? String(c.message.content) : '')
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return (
|
|||
|
|
(data && data.message && data.message.content != null ? String(data.message.content) : '') ||
|
|||
|
|
(data && data.content != null ? String(data.content) : '') ||
|
|||
|
|
(data && data.answer != null ? String(data.answer) : '') ||
|
|||
|
|
(data && data.response != null ? String(data.response) : '') ||
|
|||
|
|
(data && data.data != null ? String(data.data) : '')
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const mergedMessageFull = msgList
|
|||
|
|
.map((m) => {
|
|||
|
|
const role = m && m.role ? String(m.role) : ''
|
|||
|
|
const content = m && m.content != null ? String(m.content) : ''
|
|||
|
|
if (!role) return content
|
|||
|
|
return `${role}: ${content}`
|
|||
|
|
})
|
|||
|
|
.filter(Boolean)
|
|||
|
|
.join('\n')
|
|||
|
|
|
|||
|
|
const mergedMessage = compressMessage(extractPlainText(mergedMessageFull))
|
|||
|
|
|
|||
|
|
uni.request({
|
|||
|
|
url: apiUrl,
|
|||
|
|
method: 'POST',
|
|||
|
|
header: { 'Content-Type': 'application/json' },
|
|||
|
|
data: {
|
|||
|
|
model: String(model || ''),
|
|||
|
|
message: mergedMessage
|
|||
|
|
},
|
|||
|
|
success: (res) => {
|
|||
|
|
if (res && res.statusCode && res.statusCode >= 400) {
|
|||
|
|
reject(new Error('模型请求失败: ' + res.statusCode))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
const content = parseContent(res && res.data != null ? res.data : null)
|
|||
|
|
if (!content) {
|
|||
|
|
reject(new Error('模型返回为空'))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
resolve(content)
|
|||
|
|
},
|
|||
|
|
fail: (err) => reject(err)
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
},
|
|||
|
|
fetchCenterVideo() {
|
|||
|
|
return request({ url: '/api/homepage/video', method: 'GET' })
|
|||
|
|
.then((res) => {
|
|||
|
|
const data = res && res.data ? res.data : null
|
|||
|
|
if (!data || data.code !== 200) return
|
|||
|
|
const url = data.url || (data.data && data.data.url ? data.data.url : '')
|
|||
|
|
this.centerVideoUrl = url || ''
|
|||
|
|
})
|
|||
|
|
.catch(() => {})
|
|||
|
|
},
|
|||
|
|
toggleAiChat() {
|
|||
|
|
this.aiChatOpen = !this.aiChatOpen
|
|||
|
|
if (this.aiChatOpen) {
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
this.scrollAiChatToBottom()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
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>
|