<template>
	<!-- 主内容区域 - 登录时禁用交互 -->
	<view class="page" v-if="!isH5" :class="{ 'page-disabled': showLoginModal }">
		<view class="section">
			<view class="section-title">核心功能</view>
			<view class="grid">
				<view class="card card-core card-warning" @tap="goWarning">
					<view class="card-accent"></view>
					<view class="card-icon card-icon-solid">
						<uni-icons class="card-icon-inner" type="notification" size="28" color="#FFFFFF"></uni-icons>
					</view>
					<view class="card-title">预警中心</view>
					<view class="card-desc">查看预警、处置与跟进</view>
				</view>
				<view class="card card-core card-profile" @tap="goProfile">
					<view class="card-accent"></view>
					<view class="card-icon card-icon-solid">
						<uni-icons class="card-icon-inner" type="person" size="28" color="#FFFFFF"></uni-icons>
					</view>
					<view class="card-title">个体画像</view>
					<view class="card-desc">多量表趋势与风险提示</view>
				</view>
			</view>
		</view>

		<view class="section">
			<view class="section-title">分析工具</view>
			<view class="grid">
				<view class="card card-sub card-analysis card-purple" @tap="goSentenceCrimeAnalysis">
					<view class="card-accent card-accent-purple"></view>
					<view class="card-icon">
						<uni-icons class="card-icon-inner" type="list" size="26" color="#6F63D9"></uni-icons>
					</view>
					<view class="card-title">刑期 / 罪名分析</view>
					<view class="card-desc">对比曲线与阈值趋势</view>
				</view>
				<view class="card card-sub card-analysis card-blue" @tap="goComprehensive">
					<view class="card-accent card-accent-blue"></view>
					<view class="card-icon">
						<uni-icons class="card-icon-inner" type="paperplane" size="26" color="#3B82F6"></uni-icons>
					</view>
					<view class="card-title">综合报告</view>
					<view class="card-desc">报表汇总与折叠查看</view>
				</view>
			</view>
		</view>

		<view class="section">
			<view class="section-title">工具 & 基础功能</view>
			<view class="grid">
				<view class="card card-sub card-tools" @tap="goVoice">
					<view class="card-accent card-accent-primary"></view>
					<view class="card-icon">
						<uni-icons class="card-icon-inner" type="mic" size="26" color="#1677ff"></uni-icons>
					</view>
					<view class="card-title">语音助手</view>
					<view class="card-desc">按住说话，转写执行</view>
					<view class="fold" @tap.stop="toggleVoiceTips">
						<view class="fold-header">
							<view class="fold-title">快捷指令示例</view>
							<uni-icons :type="voiceTipsOpen ? 'top' : 'bottom'" size="14" color="#64748B"></uni-icons>
						</view>
						<view v-if="voiceTipsOpen" class="fold-body">
							<view class="fold-item">“打开预警中心”</view>
							<view class="fold-item">“查看3号监区本月数据”</view>
							<view class="fold-item">“查询高风险人员名单”</view>
						</view>
					</view>
				</view>

				<view class="card card-sub card-tools card-orange" @tap="goChartTemplates">
					<view class="card-accent card-accent-orange"></view>
					<view class="card-icon">
						<uni-icons class="card-icon-inner" type="bars" size="26" color="#F59E0B"></uni-icons>
					</view>
					<view class="card-title">自定义图表</view>
					<view class="card-desc">维度选择、模板保存与下钻</view>
				</view>

				<view class="card card-sub card-group card-wide" @tap="goInterventionTasks">
					<view class="card-accent card-accent-slate"></view>
					<view class="card-icon">
						<uni-icons class="card-icon-inner" type="flag" size="26" color="#6366F1"></uni-icons>
					</view>
					<view class="card-title">任务 & 筛选</view>
					<view class="card-desc">任务处置、标签筛人、监区概览</view>
					<view class="mini-actions" @tap.stop>
						<view class="mini" @tap="goInterventionTasks">
							<uni-icons type="notification" size="16" color="#6366F1"></uni-icons>
							<view class="mini-text">干预任务</view>
						</view>
						<view class="mini" @tap="goTagFilter">
							<uni-icons type="compose" size="16" color="#6366F1"></uni-icons>
							<view class="mini-text">标签筛选</view>
						</view>
						<view class="mini" @tap="goDashboard">
							<uni-icons type="home" size="16" color="#6366F1"></uni-icons>
							<view class="mini-text">监区看板</view>
						</view>
					</view>
				</view>

			</view>
		</view>
	</view>

	<view v-else class="page big">
		<view class="big-top">
			<view class="big-title"><text class="big-title-text">AI心理大数据平台</text></view>
		</view>
		<view class="big-top-actions">
			<view class="big-top-action-row">
				<view class="big-top-action-item" @tap="goWarning">
					<image class="big-top-action-ico" src="/static/5.png" mode="aspectFit" />
				</view>
				<view class="big-top-action-item" @tap="goProfile">
					<image class="big-top-action-ico" src="/static/4.png" mode="aspectFit" />
				</view>
				<view class="big-top-action-item" @tap="goComprehensive">
					<image class="big-top-action-ico" src="/static/3.png" mode="aspectFit" />
				</view>
				<view class="big-top-action-item" @tap="goTagFilter">
					<image class="big-top-action-ico" src="/static/1.png" mode="aspectFit" />
				</view>
				<view class="big-top-action-item" @tap="goInterventionTasks">
					<image class="big-top-action-ico" src="/static/6.png" mode="aspectFit" />
				</view>
				<view class="big-top-action-item" @tap="goNotice">
					<image class="big-top-action-ico" src="/static/7.png" mode="aspectFit" />
				</view>
			</view>
		</view>

		<view v-if="bigErrorMsg" class="big-error">{{ bigErrorMsg }}</view>

		<view class="big-grid">
			<view class="big-col">
				<view class="big-panel">
					<view class="big-panel-head">
						<view class="big-panel-title">分析工具</view>
						<view class="big-panel-sub">刑期 / 罪名分析</view>
					</view>
					<view class="big-chart big-chart-xl">
						<qiun-data-charts v-if="isMounted" :key="'bigLine-' + chartKey" type="line" :opts="bigLineOpts" :chartData="bigLineData" canvasId="bigLine" />
					</view>
				</view>

				<view class="big-panel">
					<view class="big-panel-head">
						<view class="big-panel-title">数据</view>
					</view>
					<view class="big-kpis">
						<view class="big-kpi">
							<view class="big-kpi-num">{{ bigKpi.totalAssessments }}</view>
							<view class="big-kpi-lab">测评总数</view>
						</view>
						<view class="big-kpi">
							<view class="big-kpi-num-row">
								<view class="big-kpi-num">{{ bigKpi.warningReports }}</view>
								<image class="big-kpi-ico" src="/static/yujing.png" mode="aspectFit" />
							</view>
							<view class="big-kpi-lab">预警报告</view>
						</view>
						<view class="big-kpi">
							<view class="big-kpi-num">{{ bigKpi.tasks }}</view>
							<view class="big-kpi-lab">干预任务</view>
						</view>
						<view class="big-kpi">
							<view class="big-kpi-num">{{ bigKpi.pending }}</view>
							<view class="big-kpi-lab">待处理</view>
						</view>
					</view>
				</view>

				<view class="big-panel big-panel-core">
					<view class="big-panel-head">
						<view class="big-panel-title">核心功能</view>
					</view>
					<view class="big-metrics">
						<view class="big-metric">
							<view class="big-metric-label">各监区量表测试总数</view>
							<view class="big-metric-value">{{ formatBigNumber(bigDeptAssessTotal) }}</view>
						</view>
					</view>
					<view class="big-ring">
						<qiun-data-charts v-if="isMounted" :key="'bigRing-' + chartKey" type="ring" :opts="bigRingOpts" :chartData="bigRingData" canvasId="bigRing" />
						<view class="big-ring-center" @tap="goWarning">
						</view>
					</view>
				</view>
			</view>

			<view class="big-center">
				<view class="big-center-media">
					<video
						v-if="centerVideoUrl"
						class="big-center-video"
						:src="centerVideoUrl"
						controls
						autoplay
						loop
						muted
						playsinline
						show-mute-btn
					></video>
					<view v-else class="big-center-bg" aria-hidden="true"></view>
				</view>
				<view class="big-center-text">
					<view class="big-center-text-title">{{ bigCenterTextTitle }}</view>
					<view class="big-center-text-desc">{{ bigCenterTextDesc }}</view>
				</view>

				<view :class="['big-panel', 'big-panel-ai', aiChatOpen ? 'is-open' : 'is-collapsed']">
					<view class="big-panel-head big-panel-head-ai" @tap="toggleAiChat">
						<view class="big-panel-title">AI 对话</view>
						<view class="big-ai-head-actions">
							<view class="big-panel-clear" @tap.stop="clearAiChat">清空</view>
							<view class="big-panel-sub">{{ aiChatOpen ? '收起' : '展开' }}</view>
						</view>
					</view>
					<view v-if="aiChatOpen" class="big-ai-body">
						<scroll-view
							scroll-y
							:scroll-into-view="aiChatScrollInto"
							class="big-ai-messages"
						>
							<view v-if="!aiChatMessages.length" class="big-ai-empty">请输入问题，AI 将为你生成分析建议</view>
							<view
								v-for="(m, idx) in aiChatMessages"
								:key="m.id || idx"
								:class="['big-ai-msg', m.role === 'user' ? 'is-user' : 'is-ai']"
							>
								<view class="big-ai-bubble">{{ m.content }}</view>
							</view>
							<view :id="aiChatBottomId" class="big-ai-bottom"></view>
						</scroll-view>
						<view class="big-ai-input">
							<view
								class="big-ai-voice-btn"
								:class="{ active: voiceMode }"
								@tap="toggleVoiceMode"
							>
								<uni-icons type="mic-filled" size="20" :color="voiceMode ? '#00f0ff' : '#64748B'"></uni-icons>
							</view>
							<input
								class="big-ai-text"
								v-model="aiChatInput"
								:disabled="aiChatSending"
								confirm-type="send"
								placeholder="问：打开张三的报告"
								@confirm="sendAiChat"
							/>
							<view
								class="big-ai-send"
								:class="{ disabled: aiChatSending || !aiChatInputTrim }"
								@tap="sendAiChat"
							>
								发送
							</view>
						</view>
					</view>
				</view>

			</view>

			<view class="big-col">
				<view class="big-panel">
					<view class="big-panel-head">
						<view class="big-panel-title">工具&基础功能</view>
					</view>
					<view class="big-tools">
						<view class="big-tool" @tap="goVoice">
							<view class="big-tool-icon">
								<uni-icons type="mic" size="22" color="#b7f4ff"></uni-icons>
							</view>
							<view class="big-tool-text">语音助手</view>
						</view>
						<view class="big-tool" @tap="goChartTemplates">
							<view class="big-tool-icon">
								<uni-icons type="bars" size="22" color="#b7f4ff"></uni-icons>
							</view>
							<view class="big-tool-text">自定义图表</view>
						</view>
					</view>
					<view class="big-chart">
						<view v-if="bigLoading" class="big-loading">加载中...</view>
						<qiun-data-charts v-else-if="isMounted" :key="'bigLine2-' + chartKey" type="line" :opts="bigLineOpts" :chartData="bigLineData2" canvasId="bigLine2" />
					</view>
				</view>

				<view class="big-panel big-panel-portrait">
					<view class="big-panel-head">
						<view class="big-panel-title">个体画像</view>
					</view>
					<view class="big-chart">
						<view class="big-portrait-cloud">
							<view
								v-for="(w, idx) in bigPortraitWords"
								:key="w.text + '-' + idx"
								class="big-portrait-word"
								:style="getPortraitWordStyle(w, idx)"
							>
								{{ w.text }}
							</view>
						</view>
					</view>
				</view>

				<view class="big-panel big-panel-report">
					<view class="big-panel-head">
						<view class="big-panel-title">综合报告</view>
					</view>
					<view class="big-chart big-chart-xl">
						<qiun-data-charts v-if="isMounted" :key="'bigBarMid-' + chartKey" type="column" :opts="bigBarOpts" :chartData="bigBarData" canvasId="bigBarMid" />
					</view>
				</view>
			</view>
		</view>

	</view>
	
	<!-- 登录弹窗 - 放在最后确保在最上层 -->
	<LoginModal :show="showLoginModal" :closable="false" @success="onLoginSuccess" @close="showLoginModal = false" />
</template>

<script>
	import { getToken } from '../../utils/auth'
	import { getUnreadNoticeTop, markNoticeRead } from '../../api/app/notice'
	import { getUnreadMessageList, markMessageRead } from '../../api/app/message'
	import { openLink, getMessageWsUrl } from '../../utils/link'
	import { request, setAuthFailureCallback } from '../../utils/request'
	import { getBaseUrl } from '../../utils/config'
	import { getReport, listReport } from '../../api/psychology/report'
	import { getStudentOptions, getUserAssessmentSummary } from '../../api/psychology/assessment'
	import QiunDataCharts from '../../uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
	import UniIcons from '@/uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
	import LoginModal from '../../components/LoginModal.vue'

	export default {
		components: {
			QiunDataCharts,
			UniIcons,
			LoginModal
		},
		// 捕获子组件错误，防止图表错误影响整个应用
		errorCaptured(err, vm, info) {
			// 如果是图表相关的错误，静默处理
			if (err && err.message && (
				err.message.includes('firstElementChild') ||
				err.message.includes('Cannot destructure') ||
				err.message.includes('qiun-data-charts')
			)) {
				console.warn('图表渲染警告（已处理）:', err.message)
				// 返回 false 阻止错误继续传播
				return false
			}
			// 其他错误继续传播
			return true
		},
		data() {
			return {
				showLoginModal: false,
				hasCheckedLogin: false,
				isMounted: false,
				chartKey: 0,
				resizeTimer: null,
				socketOpen: false,
				connecting: false,
				voiceTipsOpen: false,
				voiceMode: false,
				recorder: null,
				mediaRecorder: null,
				mediaStream: null,
				audioChunks: [],
				isH5: false,
				centerVideoUrl: '',
				aiChatOpen: true,
				aiChatSending: false,
				aiChatInput: '',
				aiChatMessages: [],
				aiChatScrollInto: '',
				aiChatBottomId: 'aiChatBottom',
				bigLoading: false,
				bigErrorMsg: '',
				bigInboxLoading: false,
				bigInboxList: [],
				bigDeptAssessTotal: 12872,
				bigDeptStats: [],
				bigRingOpts: {
					timing: 'easeOut',
					duration: 900,
					color: ['#2B6BFF', '#00F0FF'],
					padding: [2, 2, 2, 2],
					legend: { show: false },
					extra: {
						ring: {
							ringWidth: 16,
							offsetAngle: -90,
							labelShow: false,
							activeRadius: 6,
							border: true,
							borderWidth: 2,
							borderColor: 'rgba(255,255,255,0.12)'
						}
					}
				},
				bigLineOpts: {
					timing: 'easeOut',
					duration: 1000,
					color: ['#00F0FF', '#2B6BFF'],
					padding: [16, 14, 12, 12],
					legend: {
						show: false,
						position: 'bottom',
						float: 'center',
						fontSize: 12,
						fontColor: 'rgba(220, 250, 255, 0.85)'
					},
					xAxis: {
						disableGrid: false,
						gridType: 'dash',
						dashLength: 6,
						axisLine: true,
						axisLineColor: 'rgba(0, 240, 255, 0.12)',
						fontSize: 12,
						fontColor: 'rgba(220, 250, 255, 0.70)'
					},
					yAxis: {
						min: 0,
						splitNumber: 4,
						gridType: 'dash',
						dashLength: 6,
						fontSize: 12,
						fontColor: 'rgba(220, 250, 255, 0.70)',
						gridColor: 'rgba(0, 240, 255, 0.06)'
					},
					extra: {
						line: {
							type: 'curve',
							width: 2,
							activeType: 'hollow',
							linearType: 'custom',
							linearOpacity: 0.55,
							dataPointShape: 'circle',
							pointSize: 3
						}
					}
				},
				bigPortraitOpts: {
					timing: 'easeOut',
					duration: 1000,
					color: ['#00F0FF'],
					padding: [12, 10, 8, 8],
					legend: { show: false },
					xAxis: {
						disableGrid: false,
						gridType: 'dash',
						dashLength: 6,
						axisLine: true,
						axisLineColor: 'rgba(0, 240, 255, 0.10)',
						fontSize: 11,
						fontColor: 'rgba(220, 250, 255, 0.66)'
					},
					yAxis: {
						min: 0,
						splitNumber: 3,
						gridType: 'dash',
						dashLength: 6,
						fontSize: 11,
						fontColor: 'rgba(220, 250, 255, 0.66)',
						gridColor: 'rgba(0, 240, 255, 0.05)'
					},
					extra: {
						line: {
							type: 'curve',
							width: 2,
							activeType: 'hollow',
							linearType: 'custom',
							linearOpacity: 0.78,
							dataPointShape: 'circle',
							pointSize: 2
						}
					}
				},
				bigPortraitWords: [
					{ text: '自信', size: 44, x: 18, y: 24, color: 'rgba(88, 230, 255, 0.86)' },
					{ text: '积极', size: 56, x: 70, y: 28, color: 'rgba(120, 150, 255, 0.56)' },
					{ text: '乐观', size: 46, x: 56, y: 42, color: 'rgba(197, 142, 255, 0.78)' },
					{ text: '成长', size: 40, x: 34, y: 38, color: 'rgba(88, 230, 255, 0.82)' },
					{ text: '勇气', size: 36, x: 20, y: 46, color: 'rgba(120, 150, 255, 0.48)' },
					{ text: '专注', size: 34, x: 80, y: 44, color: 'rgba(88, 230, 255, 0.72)' },
					{ text: '自律', size: 36, x: 82, y: 58, color: 'rgba(197, 142, 255, 0.64)' },
					{ text: '感恩', size: 32, x: 62, y: 56, color: 'rgba(120, 150, 255, 0.44)' },
					{ text: '善意', size: 34, x: 24, y: 68, color: 'rgba(88, 230, 255, 0.70)' },
					{ text: '同理心', size: 38, x: 44, y: 64, color: 'rgba(197, 142, 255, 0.70)' },
					{ text: '沟通', size: 30, x: 14, y: 80, color: 'rgba(120, 150, 255, 0.40)' },
					{ text: '放松', size: 32, x: 78, y: 76, color: 'rgba(88, 230, 255, 0.64)' },
					{ text: '睡眠', size: 28, x: 68, y: 88, color: 'rgba(120, 150, 255, 0.34)' },
					{ text: '运动', size: 30, x: 34, y: 88, color: 'rgba(88, 230, 255, 0.62)' },
					{ text: '心理韧性', size: 40, x: 52, y: 80, color: 'rgba(197, 142, 255, 0.72)' },
					{ text: '边界感', size: 28, x: 52, y: 26, color: 'rgba(120, 150, 255, 0.34)' },
					{ text: '情绪稳定', size: 34, x: 40, y: 50, color: 'rgba(120, 150, 255, 0.40)' },
					{ text: '目标感', size: 30, x: 60, y: 34, color: 'rgba(88, 230, 255, 0.58)' },
					{ text: '安全感', size: 30, x: 18, y: 92, color: 'rgba(120, 150, 255, 0.30)' },
					{ text: '自我接纳', size: 32, x: 78, y: 92, color: 'rgba(197, 142, 255, 0.46)' }
				],
				bigBarOpts: {
					timing: 'easeOut',
					duration: 900,
					color: ['#00F0FF'],
					padding: [18, 22, 14, 18],
					fontColor: 'rgba(242, 252, 255, 0.92)',
					dataLabel: true,
					legend: {
						show: false
					},
					xAxis: {
						disableGrid: true,
						axisLine: true,
						axisLineColor: 'rgba(0, 240, 255, 0.12)',
						fontSize: 13,
						fontColor: 'rgba(242, 252, 255, 0.90)'
					},
					yAxis: {
						min: 0,
						splitNumber: 4,
						gridType: 'dash',
						dashLength: 4,
						fontSize: 12,
						fontColor: 'rgba(242, 252, 255, 0.78)',
						gridColor: 'rgba(0, 240, 255, 0.08)'
					},
					extra: {
						column: {
							width: 18,
							activeBgColor: '#00F0FF',
							activeBgOpacity: 0.06,
							barBorderRadius: [14, 14, 4, 4],
							linearType: 'custom',
							linearOpacity: 0.95,
							customColor: ['#2B6BFF'],
							colorStop: 0.38
						}
					}
				},
				bigRingData: { series: [{ data: [{ name: '预警', value: 0 }, { name: '正常', value: 0 }] }] },
				bigLineData: { categories: [' '], series: [{ name: '测评次数', data: [0] }] },
				bigLineData2: { categories: [' '], series: [{ name: '报告数', data: [0] }] },
				bigBarData: { categories: [' '], series: [{ name: '报告数', data: [0] }] },
				bigCenterTextTitle: 'AI 心理大数据平台 · 实时态势',
				bigCenterTextDesc: '汇聚测评、预警、干预、画像等核心数据，支持实时研判与联动处置。',
				bigKpi: {
					totalAssessments: 0,
					warningReports: 0,
					tasks: 0,
					pending: 0
				}
			}
		},
		computed: {
			aiChatInputTrim() {
				return String(this.aiChatInput || '').trim()
			}
		},
		onLoad() {
			console.log('=== onLoad 触发 ===')
			
			// 注册认证失败回调 - 显示登录弹窗
			setAuthFailureCallback(() => {
				console.log('认证失败回调触发，显示登录弹窗')
				this.showLoginModal = true
				this.bigErrorMsg = ''
			})
			
			try {
				const info = uni.getSystemInfoSync()
				console.log('系统信息:', info)
				this.isH5 = info && info.uniPlatform === 'web'
				console.log('isH5:', this.isH5)
			} catch (e) {
				console.error('获取系统信息失败:', e)
				this.isH5 = false
			}
			// 检查登录状态
			this.checkLoginStatus()
		},
		onReady() {
			// 组件渲染完成后延迟标记为已挂载，确保DOM完全准备好
			setTimeout(() => {
				this.$nextTick(() => {
					this.isMounted = true
				})
			}, 300)
			
			// 监听窗口大小变化
			// #ifdef H5
			window.addEventListener('resize', this.handleResize)
			// #endif
		},
		onUnload() {
			// 移除窗口大小变化监听
			// #ifdef H5
			window.removeEventListener('resize', this.handleResize)
			// #endif
			
			// 清理定时器
			if (this.resizeTimer) {
				clearTimeout(this.resizeTimer)
			}
		},
		onShow() {
			console.log('=== onShow 触发 ===')
			// 检查登录状态
			this.checkLoginStatus()
		},
		methods: {
			handleResize() {
				// 防抖处理：窗口大小变化时，先隐藏图表，等待调整完成后再显示
				if (this.resizeTimer) {
					clearTimeout(this.resizeTimer)
				}
				
				try {
					// 暂时隐藏图表，避免resize时的错误
					this.isMounted = false
					
					// 1200ms后重新显示图表并更新key强制重新渲染
					this.resizeTimer = setTimeout(() => {
						try {
							this.chartKey++
							// 延迟300ms再显示，确保DOM完全准备好
							setTimeout(() => {
								this.$nextTick(() => {
									this.isMounted = true
								})
							}, 300)
						} catch (e) {
							console.error('图表重新渲染失败:', e)
							// 即使出错也要恢复显示
							this.isMounted = true
						}
					}, 1200)
				} catch (e) {
					console.error('处理窗口大小变化失败:', e)
					// 出错时恢复显示
					this.isMounted = true
				}
			},
			checkLoginStatus() {
				console.log('=== checkLoginStatus 开始 ===')
				const token = getToken()
				console.log('当前token:', token ? '存在' : '不存在')
				console.log('token值:', token)
				console.log('showLoginModal当前状态:', this.showLoginModal)
				
				if (!token) {
					// 没有token，显示登录框
					console.log('没有token，准备显示登录框')
					if (!this.showLoginModal) {
						console.log('设置showLoginModal = true')
						this.showLoginModal = true
						this.hasCheckedLogin = true
					} else {
						console.log('登录框已经显示，跳过')
					}
					return false
				}
				
				console.log('有token，继续初始化')
				// 有token，标记已检查过登录
				this.hasCheckedLogin = true
				
				// 如果是首次加载（onLoad触发），初始化应用
				if (!this.socketOpen && !this.connecting) {
					console.log('初始化应用')
					this.initApp()
				}
				
				// 执行其他操作
				this.checkUnreadNoticePopup()
				this.checkUnreadMessagePopup()
				if (this.isH5) {
					this.fetchInboxList()
				}
				
				return true
			},
			initApp() {
				if (this.isH5) {
					this.fetchCenterVideo()
				}
				this.initMessageChannel()
				this.checkUnreadNoticePopup()
				this.checkUnreadMessagePopup()
				if (this.isH5) {
					this.fetchBigData()
					this.fetchInboxList()
				}
			},
			onLoginSuccess() {
				console.log('=== 登录成功回调 ===')
				// 关闭登录弹窗
				this.showLoginModal = false
				// 重置登录检查标记
				this.hasCheckedLogin = false
				// 重新检查登录状态并初始化应用
				this.checkLoginStatus()
			},
			getBailianConfig() {
				const HARDCODED_BAILIAN_API_KEY = 'sk-f991fd13fb044abebeaea81b9848c22b'
				let env = null
				try {
					env = (typeof import.meta !== 'undefined' && import.meta && import.meta.env) ? import.meta.env : null
				} catch (e) {
					env = null
				}
				const envKey = env && env.VITE_BAILIAN_API_KEY ? String(env.VITE_BAILIAN_API_KEY) : ''
				const envUrl = env && env.VITE_BAILIAN_API_URL ? String(env.VITE_BAILIAN_API_URL) : ''
				const envModel = env && env.VITE_BAILIAN_MODEL ? String(env.VITE_BAILIAN_MODEL) : ''
				const apiKey = HARDCODED_BAILIAN_API_KEY || envKey || uni.getStorageSync('BAILIAN_API_KEY')
				const apiUrl = (envUrl || uni.getStorageSync('BAILIAN_API_URL')) || 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions'
				const model = (envModel || uni.getStorageSync('BAILIAN_MODEL')) || 'qwen-plus'
				if (!apiKey) return null
				return { apiKey, apiUrl, model }
			},
			getOllamaConfig() {
				// 使用DeepSeek官方API
				const apiUrl = 'https://api.deepseek.com/v1/chat/completions'
				const apiKey = 'sk-c8e14faad3be4837a5401a3d02eaf43c'
				const model = 'deepseek-chat'
				return { apiUrl, apiKey, model }
			},
			callBailianChat({ model, apiUrl, apiKey, messages, temperature = 0.3, max_tokens = 1500 }) {
				return new Promise((resolve, reject) => {
					uni.request({
						url: apiUrl,
						method: 'POST',
						header: {
							'Content-Type': 'application/json',
							'Authorization': `Bearer ${apiKey}`
						},
						data: {
							model,
							messages,
							temperature,
							max_tokens,
							stream: false
						},
						success: (res) => {
							const data = res && res.data ? res.data : null
							const content = data && data.choices && data.choices[0] && data.choices[0].message
								? data.choices[0].message.content
								: ''
							if (!content) {
								reject(new Error('模型返回结果为空'))
								return
							}
							resolve(String(content))
						},
						fail: (err) => reject(err)
					})
				})
			},
			callOllamaChat({ model, apiUrl, messages }) {
				return new Promise((resolve, reject) => {
					const msgList = Array.isArray(messages) ? messages : []
					const extractPlainText = (input) => {
						let s = String(input || '')
						if (!s) return ''
						s = s.replace(/<script[\s\S]*?<\/script>/gi, '')
						s = s.replace(/<style[\s\S]*?<\/style>/gi, '')
						s = s.replace(/<br\s*\/?>/gi, '\n')
						s = s.replace(/<\/?p\b[^>]*>/gi, '\n')
						s = s.replace(/<\/?div\b[^>]*>/gi, '\n')
						s = s.replace(/<[^>]+>/g, '')
						s = s
							.replace(/&nbsp;/gi, ' ')
							.replace(/&lt;/gi, '<')
							.replace(/&gt;/gi, '>')
							.replace(/&amp;/gi, '&')
							.replace(/&quot;/gi, '"')
							.replace(/&#39;/gi, "'")
						s = s.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
						s = s.replace(/[ \t\f\v]+/g, ' ')
						s = s.replace(/\n\s*\n\s*\n+/g, '\n\n')
						return s.trim()
					}
					const compressMessage = (text, maxLen = 3200) => {
						const s = String(text || '')
						if (!s) return ''
						if (s.length <= maxLen) return s
						const headLen = Math.max(600, Math.floor(maxLen / 2) - 200)
						const tailLen = maxLen - headLen
						const head = s.slice(0, headLen)
						const tail = s.slice(-tailLen)
						return head + '\n...\n' + tail
					}
					const parseContent = (data) => {
						if (typeof data === 'string') return data
						if (!data || typeof data !== 'object') return ''
						// 兼容后端 AjaxResult: { code, msg, data?, content? }
						if (data && data.code != null && (data.content != null || data.data != null)) {
							const c = data.content != null ? data.content : data.data
							if (typeof c === 'string') return c
							if (c && typeof c === 'object') {
								return (
									(c && c.content != null ? String(c.content) : '') ||
									(c && c.message && c.message.content != null ? String(c.message.content) : '')
								)
							}
						}
						return (
							(data && data.message && data.message.content != null ? String(data.message.content) : '') ||
							(data && data.content != null ? String(data.content) : '') ||
							(data && data.answer != null ? String(data.answer) : '') ||
							(data && data.response != null ? String(data.response) : '') ||
							(data && data.data != null ? String(data.data) : '')
						)
					}

					const mergedMessageFull = msgList
						.map((m) => {
							const role = m && m.role ? String(m.role) : ''
							const content = m && m.content != null ? String(m.content) : ''
							if (!role) return content
							return `${role}: ${content}`
						})
						.filter(Boolean)
						.join('\n')

					const mergedMessage = compressMessage(extractPlainText(mergedMessageFull))

					uni.request({
						url: apiUrl,
						method: 'POST',
						header: { 'Content-Type': 'application/json' },
						data: {
							model: String(model || ''),
							message: mergedMessage
						},
						success: (res) => {
							if (res && res.statusCode && res.statusCode >= 400) {
								reject(new Error('模型请求失败: ' + res.statusCode))
								return
							}
							const content = parseContent(res && res.data != null ? res.data : null)
							if (!content) {
								reject(new Error('模型返回为空'))
								return
							}
							resolve(content)
						},
						fail: (err) => reject(err)
					})
				})
			},
			fetchCenterVideo() {
				return request({ url: '/api/homepage/video', method: 'GET' })
					.then((res) => {
						const data = res && res.data ? res.data : null
						if (!data || data.code !== 200) return
						const url = data.url || (data.data && data.data.url ? data.data.url : '')
						this.centerVideoUrl = url || ''
					})
					.catch(() => {})
			},
			toggleAiChat() {
				this.aiChatOpen = !this.aiChatOpen
				if (this.aiChatOpen) {
					this.$nextTick(() => {
						this.scrollAiChatToBottom()
					})
				}
			},
			toggleVoiceMode() {
				this.voiceMode = !this.voiceMode
				if (this.voiceMode) {
					uni.showToast({
						title: '语音对话已开启',
						icon: 'none',
						duration: 1500
					})
					this.startVoiceRecognition()
				} else {
					uni.showToast({
						title: '语音对话已关闭',
						icon: 'none',
						duration: 1500
					})
					this.stopVoiceRecognition()
				}
			},
			startVoiceRecognition() {
				// H5环境下使用浏览器原生录音API
				// #ifdef H5
				if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
					uni.showToast({
						title: '浏览器不支持录音功能',
						icon: 'none',
						duration: 2000
					})
					this.voiceMode = false
					return
				}
				
				navigator.mediaDevices.getUserMedia({ audio: true })
					.then(stream => {
						console.log('获取麦克风权限成功')
						this.mediaStream = stream
						this.mediaRecorder = new MediaRecorder(stream)
						this.audioChunks = []
						
						this.mediaRecorder.ondataavailable = (event) => {
							if (event.data.size > 0) {
								this.audioChunks.push(event.data)
							}
						}
						
						this.mediaRecorder.onstop = () => {
							console.log('录音停止')
							const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' })
							this.uploadAudioBlob(audioBlob)
							
							// 停止所有音轨
							if (this.mediaStream) {
								this.mediaStream.getTracks().forEach(track => track.stop())
							}
						}
						
						this.mediaRecorder.start()
						console.log('开始录音...')
					})
					.catch(err => {
						console.error('获取麦克风权限失败:', err)
						uni.showToast({
							title: '无法访问麦克风，请检查权限',
							icon: 'none',
							duration: 2000
						})
						this.voiceMode = false
					})
				// #endif
				
				// #ifndef H5
				// 检查是否支持录音
				if (typeof uni.getRecorderManager !== 'function') {
					uni.showToast({
						title: '当前环境不支持录音功能',
						icon: 'none',
						duration: 2000
					})
					this.voiceMode = false
					return
				}
				
				if (!this.recorder) {
					this.recorder = uni.getRecorderManager()
					this.recorder.onStop((res) => {
						console.log('录音停止', res)
						this.onVoiceRecorderStop(res)
					})
					this.recorder.onError((err) => {
						console.error('录音错误', err)
						this.voiceMode = false
						const msg = (err && err.errMsg) ? err.errMsg : '录音失败'
						uni.showToast({
							title: msg,
							icon: 'none'
						})
					})
					this.recorder.onStart(() => {
						console.log('录音开始')
					})
				}
				
				console.log('开始录音...')
				this.recorder.start({
					duration: 60000,
					sampleRate: 16000,
					numberOfChannels: 1,
					encodeBitRate: 96000,
					format: 'mp3'
				})
				// #endif
			},
			stopVoiceRecognition() {
				// #ifdef H5
				if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
					this.mediaRecorder.stop()
				}
				// #endif
				
				// #ifndef H5
				if (this.recorder && this.voiceMode) {
					this.recorder.stop()
				}
				// #endif
			},
			onVoiceRecorderStop(res) {
				if (!res || !res.tempFilePath) {
					uni.showToast({
						title: '未获取到录音文件',
						icon: 'none'
					})
					return
				}
				this.uploadAndAsrForAiChat(res.tempFilePath)
			},
			uploadAndAsrForAiChat(tempFilePath) {
				console.log('开始上传音频文件:', tempFilePath)
				uni.showLoading({ title: '识别中...' })
				
				const baseUrl = getBaseUrl()
				console.log('上传地址:', baseUrl + '/voice/asr')
				
				uni.uploadFile({
					url: baseUrl + '/voice/asr',
					filePath: tempFilePath,
					name: 'file',
					formData: { language: 'zh' },
					header: {
						'Authorization': getToken() || ''
					},
					success: (res) => {
						console.log('上传成功，响应:', res)
						uni.hideLoading()
						let data = null
						try {
							data = JSON.parse(res.data)
						} catch (e) {
							console.error('解析响应失败:', e)
							uni.showToast({
								title: '服务返回格式错误',
								icon: 'none'
							})
							return
						}
						
						console.log('解析后的数据:', data)
						
						if (!data || data.code !== 200) {
							uni.showToast({
								title: (data && data.msg) ? data.msg : '识别失败',
								icon: 'none'
							})
							return
						}
						
						const recognizedText = data.text || ''
						if (!recognizedText) {
							uni.showToast({
								title: '未识别到有效文本',
								icon: 'none'
							})
							return
						}
						
						console.log('识别文本:', recognizedText)
						// 将识别的文本设置到输入框并自动发送
						this.aiChatInput = recognizedText
						this.sendAiChat()
					},
					fail: (e) => {
						console.error('上传失败:', e)
						uni.hideLoading()
						uni.showToast({
							title: e && e.errMsg ? e.errMsg : '上传失败',
							icon: 'none'
						})
					}
				})
			},
			uploadAudioBlob(audioBlob) {
				console.log('开始上传音频Blob:', audioBlob)
				uni.showLoading({ title: '识别中...' })
				
				const baseUrl = getBaseUrl()
				const formData = new FormData()
				formData.append('file', audioBlob, 'audio.webm')
				formData.append('language', 'zh')
				
				fetch(baseUrl + '/voice/asr', {
					method: 'POST',
					headers: {
						'Authorization': getToken() || ''
					},
					body: formData
				})
				.then(response => response.json())
				.then(data => {
					console.log('上传成功，响应:', data)
					uni.hideLoading()
					
					if (!data || data.code !== 200) {
						uni.showToast({
							title: (data && data.msg) ? data.msg : '识别失败',
							icon: 'none'
						})
						return
					}
					
					const recognizedText = data.text || ''
					if (!recognizedText) {
						uni.showToast({
							title: '未识别到有效文本',
							icon: 'none'
						})
						return
					}
					
					console.log('识别文本:', recognizedText)
					this.aiChatInput = recognizedText
					this.sendAiChat()
				})
				.catch(err => {
					console.error('上传失败:', err)
					uni.hideLoading()
					uni.showToast({
						title: '上传失败: ' + err.message,
						icon: 'none'
					})
				})
			},
			scrollAiChatToBottom() {
				// scroll-into-view 需要一个变化的值触发
				this.aiChatScrollInto = this.aiChatBottomId + '-' + Date.now()
				this.$nextTick(() => {
					this.aiChatScrollInto = this.aiChatBottomId
				})
			},
			clearAiChat() {
				this.aiChatMessages = []
				this.aiChatInput = ''
				this.aiChatSending = false
				this.$nextTick(() => {
					this.scrollAiChatToBottom()
				})
			},
			matchAiNavigate(text) {
				const raw = String(text || '').trim()
				const s = raw.toLowerCase()
				if (!s) return null
				// 分析类问题不走本地跳转，交给模型分析逻辑
				if (this.isAiAnalyzeQuery(raw)) return null
				const reportOpenMatch = raw.match(/打开\s*([^\s的]{1,32})\s*的?报告/)
				if (reportOpenMatch && reportOpenMatch[1]) {
					const keyword = String(reportOpenMatch[1]).trim()
					return { name: '报告列表', url: '/pages/report/index?keyword=' + encodeURIComponent(keyword) }
				}
				if (/(返回|返回上一页|后退)\s*$/i.test(raw)) {
					return { name: '返回上一页', action: 'navigateBack' }
				}
				if (/(回首页|返回首页|打开首页|首页)\s*$/i.test(raw)) {
					return { name: '首页', action: 'relaunchHome' }
				}
				if (/(清空对话|清空聊天|清空会话|清空记录|重置对话|重置聊天)/.test(raw)) {
					return { name: '清空对话', action: 'clearAiChat' }
				}
				if (/(收起|展开)\s*(ai|助手|对话框)?/i.test(raw) || /(关闭ai|打开ai|关闭助手|打开助手)/i.test(raw)) {
					return { name: '切换AI面板', action: 'toggleAiChat' }
				}
				const createTaskMatch = raw.match(/(?:创建|新建)\s*(?:预警)?\s*任务\s*(\d{1,18})/)
				if (createTaskMatch && createTaskMatch[1]) {
					return { name: '创建预警任务', url: '/pages/warning/createTask?warningId=' + encodeURIComponent(createTaskMatch[1]) }
				}
				if (s.includes('未处理预警') || s.includes('待处理预警')) {
					return { name: '未处理预警', url: '/pages/warning/index?status=0' }
				}
				if (s.includes('处理中预警')) {
					return { name: '处理中预警', url: '/pages/warning/index?status=1' }
				}
				if (s.includes('严重预警') || s.includes('高危预警')) {
					return { name: '严重预警', url: '/pages/warning/index?warningLevel=严重' }
				}
				if (s.includes('高预警')) {
					return { name: '高预警', url: '/pages/warning/index?warningLevel=高' }
				}
				if (s.includes('未完成任务') || s.includes('未完成干预') || s.includes('待处理任务')) {
					return { name: '未完成干预任务', url: '/pages/interventionTask/index?status=0,1' }
				}
				if (s.includes('已完成任务') || s.includes('已完成干预')) {
					return { name: '已完成干预任务', url: '/pages/interventionTask/index?status=2' }
				}
				if (s.includes('设置') || s.includes('我的') || s.includes('个人信息')) {
					return { name: '设置', url: '/pages/settings/index' }
				}
				if (s.includes('收件箱') || s.includes('收件') || s.includes('未读消息') || s.includes('消息列表')) {
					if (s.includes('未读')) return { name: '收件箱未读', url: '/pages/message/inbox?tab=unread' }
					return { name: '收件箱', url: '/pages/message/inbox' }
				}
				if (s.includes('发件箱') || s.includes('发件')) {
					return { name: '发件箱', url: '/pages/message/outbox' }
				}
				if (s.includes('通知') || s.includes('公告')) {
					return { name: '通知公告', url: '/pages/message/notice' }
				}
				if (s.includes('综合报告历史') || s.includes('综合历史') || s.includes('历史综合报告')) {
					return { name: '综合报告历史', url: '/pages/comprehensive/history' }
				}
				if (s.includes('测评报告')) {
					return { name: '测评报告列表', url: '/pages/report/index?sourceType=assessment' }
				}
				if (s.includes('问卷报告')) {
					return { name: '问卷报告列表', url: '/pages/report/index?sourceType=questionnaire' }
				}
				if (s.includes('标签筛选') || s.includes('标签过滤') || s.includes('标签')) {
					return { name: '标签筛选', url: '/pages/profile/tagFilter' }
				}
				if (s.includes('消息发送') || s.includes('发送消息') || s.includes('群发')) {
					return { name: '消息发送', url: '/pages/message/send' }
				}
				const rules = [
					{ name: '预警中心', url: '/pages/warning/index', keys: ['预警', '风险', '告警', '预警中心'] },
					{ name: '监区看板', url: '/pages/dashboard/index', keys: ['看板', '监区', 'dashboard'] },
					{ name: '个体画像', url: '/pages/profile/index', keys: ['画像', '个体', '人员', '档案', 'profile'] },
					{ name: '综合报告', url: '/pages/comprehensive/index', keys: ['综合报告', '综合', 'comprehensive'] },
					{ name: '语音助手', url: '/pages/voice/index', keys: ['语音', '助手', 'voice'] },
					{ name: '自定义图表', url: '/pages/chart/custom', keys: ['自定义图表', '自定义', 'custom'] },
					{ name: '图表模板', url: '/pages/chart/templates', keys: ['图表模板', '模板', '图表', 'charts'] },
					{ name: '干预任务', url: '/pages/interventionTask/index', keys: ['干预', '任务', 'intervention'] },
					{ name: '测评列表', url: '/pages/assessment/index', keys: ['测评', '量表', 'assessment'] },
					{ name: '报告列表', url: '/pages/report/index', keys: ['报告', 'report'] }
				]
				for (let i = 0; i < rules.length; i++) {
					const r = rules[i]
					for (let j = 0; j < r.keys.length; j++) {
						const k = String(r.keys[j]).toLowerCase()
						if (k && s.includes(k)) return { name: r.name, url: r.url }
					}
				}
				return null
			},
			isAiAnalyzeQuery(text) {
				const s = String(text || '').trim()
				if (!s) return false
				return /(分析|趋势|总结|汇总|研判|建议)/.test(s)
			},
			parseAnalyzeReportId(text) {
				const s = String(text || '').trim()
				if (!s) return ''
				// 支持多种格式：分析123的报告、分析报告123、分析编号123、分析123
				const m = s.match(/分析\s*(?:报告|编号)?\s*(\d{1,18})(?:\s*的?报告)?/) || s.match(/(\d{1,18})/)
				return m && m[1] ? m[1] : ''
			},
			parseAnalyzeReportKeyword(text) {
				const s = String(text || '').trim()
				if (!s) return ''
				const m = s.match(/分析\s*([^\s的]{1,32})\s*的?报告/)
				if (!m || !m[1]) return ''
				const kw = String(m[1]).trim()
				if (!kw || /^\d{1,18}$/.test(kw)) return ''
				return kw
			},
			parseAnalyzeProfileKeyword(text) {
				const s = String(text || '').trim()
				if (!s) return ''
				const m = s.match(/分析\s*([^\s的]{1,32})\s*的?画像/)
				return m && m[1] ? String(m[1]).trim() : ''
			},
			buildAiAnalyzeContext() {
				return {
					overview: this.bigKpi,
					lineAssessTrend: this.bigLineData,
					lineReportTrend: this.bigLineData2,
					deptReportBar: this.bigBarData,
					deptAssessTotal: this.bigDeptAssessTotal
				}
			},
			getAiPortraitFixedMessage() {
				return '心理画像为固定值，无需分析。'
			},
			isPortraitQuery(text) {
				const s = String(text || '').trim()
				if (!s) return false
				return /(心理画像|画像)/.test(s)
			},
			getAiRefuseMessage() {
				return '抱歉，暂不提供此服务。\n你可以尝试：\n- 输入“未处理预警/严重预警/未完成干预/未读消息”等指令\n- 输入“分析XX数据/分析报告ID 123/分析张三的画像”等平台内分析指令'
			},
			getAiNotFoundMessage() {
				return '抱歉，没有查询到相关数据。请确认编号/姓名是否正确，或换一个查询条件。'
			},
			isAiInDomain(text) {
				const s = String(text || '').trim()
				if (!s) return false
				return /(预警|风险|告警|干预|任务|测评|量表|报告|画像|综合|趋势|分析|总结|研判|建议|消息|通知|公告|监区|看板|人员|档案)/.test(s)
			},
			isAiNeedContext(text) {
				const s = String(text || '').trim()
				if (!s) return true
				// 仅当属于分析类问题时才允许调用大模型
				return this.isAiAnalyzeQuery(s)
			},
			buildAiSystemPrompt() {
				return '你是“AI心理大数据平台”的内置助手，仅服务于监狱管理员与心理管理员的业务场景。\n' +
					'严格要求：\n' +
					'1) 只能基于用户提供的上下文数据(JSON)进行分析与归纳；禁止编造数据。\n' +
					'2) 只回答平台业务范围：预警、干预任务、测评、报告、消息通知、监区统计。\n' +
					'3) 回复格式：只输出答案本身；不要输出“下一步/建议操作/引导用户去哪里点/平台外链接/参考资料”。\n' +
					'4) 输出风格：中文；简洁；优先给出结论，并用少量要点说明依据；不要长篇铺陈。\n' +
					'5) 心理画像为固定值：禁止对“心理画像/画像词云”等做分析、解释或延伸；如果用户提到心理画像，一律回复“心理画像为固定值，无需分析。”\n' +
					'6) 对平台以外的问题（例如天气、新闻、股票、外部网站、百科、闲聊等）一律回复：\n' +
					'“抱歉，暂不提供此服务。”\n' +
					'7) 如果上下文缺失/无法得出结论，回复：\n' +
					'“抱歉，没有查询到相关数据。”'
			},
			sanitizeAiAnswer(answer, userText) {
				const a = String(answer || '').trim()
				if (!a) return ''
				// 只要用户问题不在平台域内，或回复包含明显平台外内容，则替换为拒答
				const userInDomain = this.isAiInDomain(userText)
				const outHint = /(http(s)?:\/\/|www\.|百度|谷歌|天气|新闻|股票|电影|音乐|游戏|菜谱|百科|维基)/i.test(a)
				if (!userInDomain || outHint) {
					return '抱歉，暂不提供此服务。'
				}
				return a
			},
			canCallAiForText(text) {
				const s = String(text || '').trim()
				if (!s) return false
				if (!this.isAiInDomain(s)) return false
				if (!this.isAiNeedContext(s)) return false
				return true
			},
			sendAiChat() {
				if (this.aiChatSending) return
				const text = this.aiChatInputTrim
				if (!text) return
				this.aiChatInput = ''
				const userMsg = { id: 'u_' + Date.now(), role: 'user', content: text }
				this.aiChatMessages = [...this.aiChatMessages, userMsg]
				this.aiChatSending = true
				this.$nextTick(() => {
					this.scrollAiChatToBottom()
				})

				const openReportIdMatch = String(text || '').trim().match(/打开\s*(?:报告\s*id\s*|报告\s*#\s*|报告#\s*)(\d{1,18})/i)
				if (openReportIdMatch && openReportIdMatch[1]) {
					const reportId = openReportIdMatch[1]
					uni.navigateTo({ url: `/pages/report/detail?reportId=${encodeURIComponent(reportId)}&sourceType=` })
					this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: '已跳转：报告详情#' + reportId }]
					this.aiChatSending = false
					this.$nextTick(() => {
						this.scrollAiChatToBottom()
					})
					return
				}

				const openInfoNumberMatch = String(text || '').trim().match(/打开\s*(\d{1,18})\s*的?报告/)
				if (openInfoNumberMatch && openInfoNumberMatch[1]) {
					const infoNumber = openInfoNumberMatch[1]
					uni.navigateTo({ url: `/pages/report/index?keyword=${encodeURIComponent(infoNumber)}` })
					this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: '已跳转：报告列表（信息编号 ' + infoNumber + '）' }]
					this.aiChatSending = false
					this.$nextTick(() => {
						this.scrollAiChatToBottom()
					})
					return
				}

				const localMatch = this.matchAiNavigate(text)
				if (localMatch && localMatch.action) {
					if (localMatch.action === 'clearAiChat') {
						this.clearAiChat()
						return
					}
					if (localMatch.action === 'toggleAiChat') {
						this.toggleAiChat()
						this.aiChatSending = false
						return
					}
					if (localMatch.action === 'navigateBack') {
						uni.navigateBack({ delta: 1 })
						this.aiChatSending = false
						return
					}
					if (localMatch.action === 'relaunchHome') {
						uni.reLaunch({ url: '/pages/index/index' })
						this.aiChatSending = false
						return
					}
				}
				if (localMatch && localMatch.url) {
					uni.navigateTo({ url: localMatch.url })
					this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: '已跳转：' + (localMatch.name || localMatch.url) }]
					this.aiChatSending = false
					this.$nextTick(() => {
						this.scrollAiChatToBottom()
					})
					return
				}

				const bailianCfg = this.getBailianConfig()
				const ollamaCfg = this.getOllamaConfig()
				// DeepSeek官方API使用OpenAI兼容格式
				const llmCfg = bailianCfg 
					? { type: 'bailian', ...bailianCfg } 
					: (ollamaCfg && ollamaCfg.apiKey 
						? { type: 'bailian', ...ollamaCfg }  // DeepSeek API使用OpenAI格式
						: (ollamaCfg ? { type: 'ollama', ...ollamaCfg } : null))
				if (llmCfg) {
					if (this.isPortraitQuery(text)) {
						this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiPortraitFixedMessage() }]
						this.aiChatSending = false
						this.$nextTick(() => {
							this.scrollAiChatToBottom()
						})
						return
					}
					if (!this.canCallAiForText(text)) {
						this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiRefuseMessage() }]
						this.aiChatSending = false
						this.$nextTick(() => {
							this.scrollAiChatToBottom()
						})
						return
					}
					const reportId = this.parseAnalyzeReportId(text)
					const reportKeyword = this.parseAnalyzeReportKeyword(text)
					const profileKeyword = ''
					const baseContext = this.buildAiAnalyzeContext()
					let report = null
					let profile = null
					const p1 = reportId
						? listReport({ pageNum: 1, pageSize: 1, infoNumber: reportId }).then((res) => {
							const data = res && res.data ? res.data : null
							if (!data || data.code !== 200) return
							const rows = data.rows || []
							if (!Array.isArray(rows) || rows.length === 0) return
							const rid = rows[0].reportId
							if (!rid) return
							return getReport(rid, rows[0].sourceType || '').then((rr) => {
								const dd = rr && rr.data ? rr.data : null
								if (!dd || dd.code !== 200) return
								const r = dd.data || {}
								report = {
									reportId: rid,
									reportTitle: r.reportTitle || r.reportType || '',
									reportContent: r.reportContent || ''
								}
							}).catch(() => {})
						}).catch(() => {})
						: (reportKeyword
							? getStudentOptions({ keyword: reportKeyword, limit: 1 }).then((res) => {
								const data = res && res.data ? res.data : null
								if (!data || data.code !== 200) return
								const list = data.data || []
								if (!Array.isArray(list) || list.length === 0) return
								const userId = list[0].userId
								if (!userId) return
								return listReport({ pageNum: 1, pageSize: 1, userId }).then((lr) => {
									const d = lr && lr.data ? lr.data : null
									if (!d || d.code !== 200) return
									const rows = d.rows || []
									if (!Array.isArray(rows) || rows.length === 0) return
									const rid = rows[0].reportId
									if (!rid) return
									return getReport(rid, '').then((rr) => {
										const dd = rr && rr.data ? rr.data : null
										if (!dd || dd.code !== 200) return
										const r = dd.data || {}
										report = {
											reportId: rid,
											reportTitle: r.reportTitle || r.reportType || '',
											reportContent: r.reportContent || ''
										}
									}).catch(() => {})
								}).catch(() => {})
							}).catch(() => {})
							: Promise.resolve())
					const p2 = profileKeyword
						? getStudentOptions({ keyword: profileKeyword, limit: 1 }).then((res) => {
							const data = res && res.data ? res.data : null
							if (!data || data.code !== 200) return
							const list = data.data || []
							if (!Array.isArray(list) || list.length === 0) return
							const userId = list[0].userId
							if (!userId) return
							return getUserAssessmentSummary(userId).then((res2) => {
								const d2 = res2 && res2.data ? res2.data : null
								if (!d2 || d2.code !== 200) return
								profile = { keyword: profileKeyword, userId, summary: d2.data || {} }
							}).catch(() => {})
						}).catch(() => {})
						: Promise.resolve()
					Promise.all([p1, p2]).then(() => {
						// 如果既没有报告也没有画像/概览诉求，则不调用大模型
						if (!reportId && !reportKeyword && !profileKeyword && !/(概览|看板|面板|仪表盘|当前面板|面板数据|当前数据|当前看板|总体|全局)/.test(String(text || ''))) {
							throw new Error('__NO_CONTEXT__')
						}
						if ((reportId || reportKeyword) && report && !report.reportContent) {
							throw new Error('__NOT_FOUND__')
						}
						if (profileKeyword && !profile) {
							throw new Error('__NOT_FOUND__')
						}
						const systemPrompt = this.buildAiSystemPrompt()
						const ctx = { ...baseContext, report, profile }
						const userPayload =
							'用户问题：' + text + '\n\n' +
							'上下文数据(JSON)：' + JSON.stringify(ctx)
						const messages = [
							{ role: 'system', content: systemPrompt },
							{ role: 'user', content: userPayload }
						]
						if (llmCfg.type === 'bailian') {
							return this.callBailianChat({
								model: llmCfg.model,
								apiUrl: llmCfg.apiUrl,
								apiKey: llmCfg.apiKey,
								messages
							})
						}
						return this.callOllamaChat({
							model: llmCfg.model,
							apiUrl: llmCfg.apiUrl,
							messages
						})
					}).then((content) => {
						const safe = this.sanitizeAiAnswer(content, text)
						this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: safe || this.getAiNotFoundMessage() }]
					}).catch((e) => {
						const msg = e && e.message ? e.message : ''
						if (msg === '__NO_CONTEXT__') {
							this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiRefuseMessage() }]
							return
						}
						if (msg === '__NOT_FOUND__') {
							this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiNotFoundMessage() }]
							return
						}
						const safe = this.sanitizeAiAnswer(msg, text)
						this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: safe || '网络错误' }]
					}).finally(() => {
						this.aiChatSending = false
						this.$nextTick(() => {
							this.scrollAiChatToBottom()
						})
					})
					return
				}

				if (this.isAiAnalyzeQuery(text)) {
					if (!this.isAiInDomain(text)) {
						this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiRefuseMessage() }]
						this.aiChatSending = false
						this.$nextTick(() => {
							this.scrollAiChatToBottom()
						})
						return
					}
					const reportId = this.parseAnalyzeReportId(text)
					const profileKeyword = this.parseAnalyzeProfileKeyword(text)
					if (!reportId && !profileKeyword && !/(概览|看板|面板|仪表盘|当前面板|面板数据|当前数据|当前看板|总体|全局)/.test(String(text || ''))) {
						this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: this.getAiRefuseMessage() }]
						this.aiChatSending = false
						this.$nextTick(() => {
							this.scrollAiChatToBottom()
						})
						return
					}
					const baseContext = this.buildAiAnalyzeContext()
					let report = null
					let profile = null

					const p1 = reportId
						? listReport({ pageNum: 1, pageSize: 1, infoNumber: reportId }).then((res) => {
							const data = res && res.data ? res.data : null
							if (!data || data.code !== 200) return
							const rows = data.rows || []
							if (!Array.isArray(rows) || rows.length === 0) return
							const rid = rows[0].reportId
							if (!rid) return
							return getReport(rid, rows[0].sourceType || '').then((rr) => {
								const dd = rr && rr.data ? rr.data : null
								if (!dd || dd.code !== 200) return
								const r = dd.data || {}
								report = {
									reportId: rid,
									reportTitle: r.reportTitle || r.reportType || '',
									reportContent: r.reportContent || ''
								}
							}).catch(() => {})
						}).catch(() => {})
						: Promise.resolve()

					const p2 = profileKeyword
						? getStudentOptions({ keyword: profileKeyword, limit: 1 }).then((res) => {
							const data = res && res.data ? res.data : null
							if (!data || data.code !== 200) return
							const list = data.data || []
							if (!Array.isArray(list) || list.length === 0) return
							const userId = list[0].userId
							if (!userId) return
							return getUserAssessmentSummary(userId).then((res2) => {
								const d2 = res2 && res2.data ? res2.data : null
								if (!d2 || d2.code !== 200) return
								profile = { keyword: profileKeyword, userId, summary: d2.data || {} }
							}).catch(() => {})
						}).catch(() => {})
						: Promise.resolve()

					Promise.all([p1, p2]).then(() => {
						return request({
							url: '/voice/analyze',
							method: 'POST',
							data: { text, context: { ...baseContext, report, profile } }
						})
					}).then((res) => {
						const data = res && res.data ? res.data : null
						if (!data || data.code !== 200) {
							const msg = (data && data.msg) ? data.msg : 'AI 对话失败'
							this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: msg }]
							return
						}
						const payload = data.payload || {}
						const content = payload.result || payload.message || data.msg || ''
						this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: content || '暂无回复' }]
					}).catch((e) => {
						const msg = e && e.message ? e.message : '网络错误'
						this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: msg }]
					}).finally(() => {
						this.aiChatSending = false
						this.$nextTick(() => {
							this.scrollAiChatToBottom()
						})
					})
					return
				}

				request({
					url: '/voice/command',
					method: 'POST',
					data: { text }
				}).then((res) => {
					const data = res && res.data ? res.data : null
					if (data && data.code === 200 && data.type === 'navigate' && data.payload && data.payload.url) {
						uni.navigateTo({ url: data.payload.url })
						this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: '已跳转：' + data.payload.url }]
						return true
					}
					return false
				}).then((navigated) => {
					if (navigated) return
					this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: '暂时无法回答，请尝试“分析XX数据”或输入可跳转指令。' }]
				}).catch((e) => {
					const msg = e && e.message ? e.message : '网络错误'
					this.aiChatMessages = [...this.aiChatMessages, { id: 'a_' + Date.now(), role: 'ai', content: msg }]
				}).finally(() => {
					this.aiChatSending = false
					this.$nextTick(() => {
						this.scrollAiChatToBottom()
					})
				})
			},
			getPortraitWordStyle(w, idx) {
				const baseLeft = typeof w.x === 'number' ? w.x : 50
				const baseTop = typeof w.y === 'number' ? w.y : 50
				const size = typeof w.size === 'number' ? w.size : 28
				const hue = idx % 3
				const anim = (idx % 4) + 1
				const delay = (idx % 7) * 0.18
				const drift = 0.6 + (idx % 5) * 0.18
				const fallbackColor =
					hue === 0
						? 'rgba(88, 230, 255, 0.86)'
						: hue === 1
							? 'rgba(197, 142, 255, 0.56)'
							: 'rgba(120, 150, 255, 0.52)'
				return {
					left: baseLeft + '%',
					top: baseTop + '%',
					fontSize: size + 'rpx',
					color: w.color || fallbackColor,
					opacity: w.opacity != null ? w.opacity : 1,
					transform: 'translate(-50%, -50%)',
					animationName: 'portraitFloat' + anim,
					animationDuration: drift.toFixed(2) + 's',
					animationTimingFunction: 'ease-in-out',
					animationIterationCount: 'infinite',
					animationDelay: delay.toFixed(2) + 's'
				}
			},
			fetchBigData() {
				// 检查是否有token，没有token不发起请求
				const token = getToken()
				if (!token) {
					console.log('fetchBigData: 没有token，跳过请求')
					this.applyBigDemoData()
					return
				}
				
				console.log('fetchBigData: 开始请求，token存在')
				if (this.bigLoading) return
				this.bigLoading = true
				this.bigErrorMsg = ''
				Promise.all([
					request({ url: '/psychology/assessment/analytics', method: 'GET' }),
					request({ url: '/psychology/assessment/deptOverview?topN=7', method: 'GET' })
				])
					.then(([aRes, dRes]) => {
						this.bigLoading = false
						console.log('fetchBigData: analytics响应 code:', aRes.data?.code)
						const aData = aRes && aRes.data ? aRes.data : null
						const dData = dRes && dRes.data ? dRes.data : null
						
						// 认证失败已在request.js中统一处理，这里只处理其他错误
						if (!aData || aData.code !== 200) {
							const errorMsg = (aData && aData.msg) ? aData.msg : '概览加载失败'
							console.error('fetchBigData: 请求失败，code:', aData ? aData.code : 'null')
							// 如果是认证失败，不显示错误消息（已在request.js中处理）
							if (aData && (aData.code === 401 || aData.code === 403)) {
								this.bigErrorMsg = ''
							} else {
								this.bigErrorMsg = errorMsg
							}
							this.applyBigDemoData()
							return
						}
						const analytics = aData.data || {}
						const overview = analytics.overview || {}
						this.bigRingData = this.buildRingData(overview)
						this.bigLineData = this.buildTrendLineData(analytics)
						this.bigLineData2 = this.buildTrendReportLineData(analytics)
						const deptRows = (dData && dData.code === 200) ? (dData.data || []) : []
						this.bigBarData = this.buildDeptBarData(deptRows)
						this.bigDeptAssessTotal = this.buildDeptAssessTotal(deptRows, overview)
						this.bigDeptStats = this.buildDeptStats(deptRows)
						this.bigKpi = this.buildKpi(overview)
					})
					.catch((e) => {
						this.bigLoading = false
						console.error('fetchBigData: 请求异常', e)
						this.bigErrorMsg = e && e.message ? e.message : '网络错误'
						this.applyBigDemoData()
					})
			},
			applyBigDemoData() {
				const emptyOverview = {}
				const emptyAnalytics = {}
				this.bigRingData = this.buildRingData(emptyOverview)
				this.bigLineData = this.buildTrendLineData(emptyAnalytics)
				this.bigLineData2 = this.buildTrendReportLineData(emptyAnalytics)
				this.bigBarData = this.buildDeptBarData([])
				this.bigDeptAssessTotal = this.buildDeptAssessTotal([], emptyOverview)
				this.bigDeptStats = this.buildDeptStats([])
				this.bigKpi = this.buildKpi(emptyOverview)
			},
			buildDeptStats(rows) {
				const list = Array.isArray(rows) ? rows : []
				if (!list.length) {
					return [
						{ name: '一监区', value: 18 },
						{ name: '二监区', value: 26 },
						{ name: '三监区', value: 14 },
						{ name: '四监区', value: 32 },
						{ name: '五监区', value: 22 },
						{ name: '六监区', value: 28 }
					]
				}
				const mapped = list
					.map((r) => {
						const name = r.deptName || String(r.deptId || '')
						const v = Number(r.totalAssessments || r.assessments || r.assessmentCount || r.testCount || r.total || r.generatedReports || 0)
						return { name, value: isNaN(v) ? 0 : v }
					})
					.filter((it) => it.name)
				return mapped.slice(0, 10)
			},
			buildDeptAssessTotal(rows, overview) {
				const list = Array.isArray(rows) ? rows : []
				if (list.length) {
					const sum = list.reduce((acc, r) => {
						const v = Number(
							r.totalAssessments || r.assessments || r.assessmentCount || r.testCount || r.total || 0
						)
						return acc + (isNaN(v) ? 0 : v)
					}, 0)
					if (sum > 0) return sum
				}
				const fallback = Number(overview && (overview.totalAssessments || overview.assessmentTotal || overview.testTotal) || 0)
				if (fallback > 0) return fallback
				return 12872
			},
			formatBigNumber(n) {
				const num = Number(n || 0)
				if (!num) return '0'
				return String(num).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
			},
			buildRingData(overview) {
				const total = Number(overview.generatedReports || 0)
				const warning = Number(overview.warningReports || 0)
				const normal = Math.max(total - warning, 0)
				if (total <= 0 && warning <= 0 && normal <= 0) {
					return {
						series: [{
							data: [
								{ name: '预警', value: 12 },
								{ name: '正常', value: 88 }
							]
						}]
					}
				}
				return {
					series: [{
						data: [
							{ name: '预警', value: warning },
							{ name: '正常', value: normal }
						]
					}]
				}
			},
			buildTrendLineData(analytics) {
				const list = analytics && analytics.monthlyTrend ? analytics.monthlyTrend : []
				if (!list || !list.length) {
					const categories = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
					const data = [32, 45, 38, 52, 48, 66, 58, 74, 60, 82, 76, 90]
					return { categories, series: [{ name: '测评次数', data }] }
				}
				const categories = list.map((it) => it.label)
				const data = list.map((it) => Number(it.value || 0))
				return { categories, series: [{ name: '测评次数', data }] }
			},
			buildTrendReportLineData(analytics) {
				const list = (analytics && analytics.monthlyReportTrend)
					? analytics.monthlyReportTrend
					: ((analytics && analytics.deptReportTrend) ? analytics.deptReportTrend : [])
				if (!list || !list.length) {
					const categories = ['01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12']
					const data = [6, 10, 8, 12, 9, 14, 11, 16, 13, 18, 15, 20]
					return { categories, series: [{ name: '报告数', data }] }
				}
				const categories = list.map((it) => it.label)
				const data = list.map((it) => Number(it.value || 0))
				return { categories, series: [{ name: '报告数', data }] }
			},
			buildDeptBarData(rows) {
				const list = Array.isArray(rows) ? rows : []
				if (!list.length) {
					const categories = ['一监区', '二监区', '三监区', '四监区', '五监区', '六监区']
					const data = [18, 26, 14, 32, 22, 28]
					return { categories, series: [{ name: '报告数', data }] }
				}
				const categories = list.map((r) => r.deptName || String(r.deptId || ''))
				const data = list.map((r) => Number(r.generatedReports || 0))
				return { categories, series: [{ name: '报告数', data }] }
			},
			buildKpi(overview) {
				const totalAssessments = Number(overview.totalAssessments || 0)
				const warningReports = Number(overview.warningReports || 0)
				const tasks = Number(overview.interventionTasks || 0)
				const pending = Number(overview.pendingTasks || 0)
				if (totalAssessments <= 0 && warningReports <= 0 && tasks <= 0 && pending <= 0) {
					return {
						totalAssessments: 1287,
						warningReports: 36,
						tasks: 19,
						pending: 7
					}
				}
				return { totalAssessments, warningReports, tasks, pending }
			},
			toggleVoiceTips() {
				this.voiceTipsOpen = !this.voiceTipsOpen
			},
			fetchInboxList() {
				// 检查是否有token，没有token不发起请求
				const token = getToken()
				if (!token) {
					console.log('fetchInboxList: 没有token，跳过请求')
					this.bigInboxList = []
					return
				}
				
				if (this.bigInboxLoading) return
				this.bigInboxLoading = true
				getUnreadMessageList(6)
					.then((res) => {
						const data = res && res.data ? res.data : null
						if (!data || data.code !== 200) return
						const list = data.data || []
						this.bigInboxList = Array.isArray(list) ? list.slice(0, 6) : []
					})
					.catch(() => {
						this.bigInboxList = []
					})
					.finally(() => {
						this.bigInboxLoading = false
					})
			},
			openInboxItem(m) {
				if (!m) return
				const msgId = m.msgId
				const title = m.title || '消息'
				const linkUrl = m.linkUrl || ''
				if (msgId) {
					markMessageRead(msgId).catch(() => {})
				}
				if (linkUrl) {
					openLink(linkUrl, { title })
					return
				}
				this.showMessagePopup(m)
			},
			checkUnreadMessagePopup() {
				getUnreadMessageList(1)
					.then((res) => {
						const data = res && res.data ? res.data : null
						if (!data || data.code !== 200) return
						const list = data.data || []
						if (!Array.isArray(list) || !list.length) return
						this.showMessagePopup(list[0])
					})
					.catch(() => {})
			},
			initMessageChannel() {
				if (this.socketOpen || this.connecting) return
				const token = getToken()
				if (!token) return
				const wsUrl = getMessageWsUrl(token)
				if (!wsUrl) return
				this.connecting = true
				uni.connectSocket({ url: wsUrl })
				uni.onSocketOpen(() => {
					this.socketOpen = true
					this.connecting = false
				})
				uni.onSocketClose(() => {
					this.socketOpen = false
					this.connecting = false
				})
				uni.onSocketError(() => {
					this.socketOpen = false
					this.connecting = false
				})
				uni.onSocketMessage((res) => {
					this.handleSocketMessage(res)
				})
			},
			handleSocketMessage(res) {
				let data = null
				try {
					data = res && res.data ? JSON.parse(res.data) : null
				} catch (e) {
					data = null
				}
				if (!data || !data.type) return
				if (data.type === 'notice:new') {
					this.showNoticePopup(data.payload)
					return
				}
				if (data.type === 'message:new') {
					this.showMessagePopup(data.payload)
					return
				}
			},
			checkUnreadNoticePopup() {
				getUnreadNoticeTop(1)
					.then((res) => {
						const data = res && res.data ? res.data : null
						if (!data || data.code !== 200) return
						const notice = data.data
						if (!notice || !notice.noticeId) return
						this.showNoticePopup(notice)
					})
					.catch(() => {})
			},
			showNoticePopup(notice) {
				if (!notice) return
				const noticeId = notice.noticeId
				const title = notice.title || '通知'
				const content = notice.content || ''
				const linkUrl = notice.linkUrl || ''
				uni.showModal({
					title,
					content,
					confirmText: linkUrl ? '查看' : '知道了',
					cancelText: '关闭',
					success: (r) => {
						if (noticeId) {
							markNoticeRead(noticeId).catch(() => {})
						}
						if (r && r.confirm && linkUrl) {
							openLink(linkUrl, { title })
						}
					}
				})
			},
			showMessagePopup(msg) {
				if (!msg) return
				const msgId = msg.msgId
				const title = msg.title || '消息'
				const content = msg.content || ''
				const linkUrl = msg.linkUrl || ''
				const bizType = msg.bizType || ''
				if (bizType === 'warning:urgent') {
					try {
						if (typeof window !== 'undefined' && window.AndroidTTS && typeof window.AndroidTTS.speak === 'function') {
							window.AndroidTTS.speak(content)
						}
					} catch (e) {}
				}
				uni.showModal({
					title,
					content,
					confirmText: linkUrl ? '查看' : '知道了',
					cancelText: '关闭',
					success: (r) => {
						if (msgId) {
							markMessageRead(msgId).catch(() => {})
						}
						if (r && r.confirm && linkUrl) {
							openLink(linkUrl, { title })
						}
					}
				})
			},
			goSentenceCrimeAnalysis() {
				uni.navigateTo({
					url: '/pages/chart/sentenceCrimeAnalysis'
				})
			},
			goInterventionTasks() {
				uni.navigateTo({
					url: '/pages/interventionTask/index'
				})
			},
			goChartTemplates() {
				uni.navigateTo({
					url: '/pages/chart/templates'
				})
			},
			goTagFilter() {
				uni.navigateTo({
					url: '/pages/profile/tagFilter'
				})
			},
			goComprehensive() {
				uni.navigateTo({
					url: '/pages/comprehensive/index'
				})
			},
			goVoice() {
				uni.navigateTo({
					url: '/pages/voice/index'
				})
			},
			goWarning() {
				uni.navigateTo({
					url: '/pages/warning/index'
				})
			},
			goProfile() {
				uni.navigateTo({
					url: '/pages/profile/index'
				})
			},
			goDashboard() {
				uni.navigateTo({
					url: '/pages/dashboard/index'
				})
			},
			goNotice() {
				uni.navigateTo({
					url: '/pages/message/notice'
				})
			},
			goSettings() {
				uni.navigateTo({
					url: '/pages/settings/index'
				})
			}
		}
	}
</script>

<style scoped>
	/* 自适应字体大小变量 - 基于视口宽度 */
	:root {
		--font-xs: calc(22rpx + 0.3vw);   /* 小字体 */
		--font-sm: calc(24rpx + 0.35vw);  /* 小字体 */
		--font-base: calc(26rpx + 0.4vw); /* 基础字体 */
		--font-md: calc(28rpx + 0.45vw);  /* 中等字体 */
		--font-lg: calc(32rpx + 0.6vw);   /* 大字体 */
		--font-xl: calc(38rpx + 0.75vw);  /* 超大字体 */
		--font-2xl: calc(56rpx + 1.2vw);  /* 特大字体 */
		--font-3xl: calc(60rpx + 1.5vw);  /* 巨大字体 */
	}
	
	.page {
		min-height: 100vh;
		padding: 24rpx 24rpx 0;
		box-sizing: border-box;
		background: #F4F6FB;
		--c-primary: #1677ff;
		--c-danger: #E87A7A;
		/* 页面级字体变量 */
		--font-xs: calc(22rpx + 0.3vw);
		--font-sm: calc(24rpx + 0.35vw);
		--font-base: calc(26rpx + 0.4vw);
		--font-md: calc(28rpx + 0.45vw);
		--font-lg: calc(32rpx + 0.6vw);
		--font-xl: calc(38rpx + 0.75vw);
		--font-2xl: calc(56rpx + 1.2vw);
		--font-3xl: calc(60rpx + 1.5vw);
	}

	.section {
		margin-top: 14rpx;
	}
	.section-title {
		font-size: var(--font-md);
		font-weight: 700;
		color: #111827;
		margin: 10rpx 6rpx 14rpx;
	}

	.grid {
		display: flex;
		flex-wrap: wrap;
		justify-content: space-between;
	}

	.card {
		width: 48%;
		background: #FFFFFF;
		border-radius: 16rpx;
		padding: 22rpx 20rpx;
		box-sizing: border-box;
		margin-bottom: 18rpx;
		border: 1px solid rgba(15, 23, 42, 0.06);
		box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.05);
		transition: transform 120ms ease, border-color 120ms ease;
	}
	.card-wide {
		width: 100%;
	}
	.card:active {
		transform: scale(0.98);
		border-color: rgba(22, 119, 255, 0.35);
	}

	.card-core {
		position: relative;
		overflow: hidden;
		padding: 26rpx 22rpx;
	}
	.card-sub {
		position: relative;
		overflow: hidden;
	}
	.card-warning {
		background: #FFFFFF;
		border: 1px solid rgba(232, 122, 122, 0.18);
		box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.05);
	}
	.card-profile {
		background: #FFFFFF;
		border: 1px solid rgba(22, 119, 255, 0.18);
		box-shadow: 0 10rpx 22rpx rgba(15, 23, 42, 0.05);
	}

	.card-accent {
		position: absolute;
		left: 0;
		top: 0;
		bottom: 0;
		width: 10rpx;
		border-top-left-radius: 16rpx;
		border-bottom-left-radius: 16rpx;
	}
	.card-warning .card-accent {
		background: linear-gradient(180deg, rgba(232, 122, 122, 0.95) 0%, rgba(232, 122, 122, 0.55) 100%);
	}
	.card-profile .card-accent {
		background: linear-gradient(180deg, rgba(22, 119, 255, 0.95) 0%, rgba(22, 119, 255, 0.55) 100%);
	}
	.card-accent-primary {
		background: linear-gradient(180deg, rgba(22, 119, 255, 0.95) 0%, rgba(22, 119, 255, 0.55) 100%);
	}
	.card-accent-blue {
		background: linear-gradient(180deg, rgba(59, 130, 246, 0.95) 0%, rgba(59, 130, 246, 0.55) 100%);
	}
	.card-accent-purple {
		background: linear-gradient(180deg, rgba(111, 99, 217, 0.95) 0%, rgba(111, 99, 217, 0.55) 100%);
	}
	.card-accent-orange {
		background: linear-gradient(180deg, rgba(245, 158, 11, 0.95) 0%, rgba(245, 158, 11, 0.55) 100%);
	}
	.card-accent-slate {
		background: linear-gradient(180deg, rgba(99, 102, 241, 0.95) 0%, rgba(99, 102, 241, 0.55) 100%);
	}

	.card-analysis,
	.card-tools,
	.card-group {
		background: #FFFFFF;
	}

	.card-icon {
		width: calc(64rpx + 1.5vh);
		height: calc(64rpx + 1.5vh);
		border-radius: 16rpx;
		margin-bottom: 16rpx;
		display: flex;
		align-items: center;
		justify-content: center;
		background: rgba(15, 23, 42, 0.06);
	}
	.card-icon-solid {
		background: rgba(22, 119, 255, 0.92);
	}
	.card-warning .card-icon-solid {
		background: rgba(232, 122, 122, 0.92);
	}
	.card-profile .card-icon-solid {
		background: rgba(22, 119, 255, 0.92);
	}
	.card-purple .card-icon {
		background: rgba(111, 99, 217, 0.12);
	}
	.card-blue .card-icon {
		background: rgba(59, 130, 246, 0.12);
	}
	.card-tools .card-icon {
		background: rgba(22, 119, 255, 0.10);
	}
	.card-orange .card-icon {
		background: rgba(245, 158, 11, 0.12);
	}
	.card-group .card-icon {
		background: rgba(99, 102, 241, 0.12);
	}
	.card-icon-inner {
		line-height: 1;
	}

	.card-title {
		font-size: var(--font-lg);
		font-weight: 700;
		color: #111827;
	}

	.card-desc {
		margin-top: 10rpx;
		font-size: var(--font-sm);
		color: #6B7280;
	}

	.fold {
		margin-top: 16rpx;
		border-radius: 14rpx;
		background: rgba(255, 255, 255, 0.7);
		border: 1px solid rgba(15, 23, 42, 0.06);
		overflow: hidden;
	}
	.fold-header {
		padding: 14rpx 14rpx;
		display: flex;
		align-items: center;
		justify-content: space-between;
	}
	.fold-title {
		font-size: var(--font-sm);
		font-weight: 700;
		color: #334155;
	}
	.fold-body {
		padding: 0 14rpx 14rpx;
	}
	.fold-item {
		font-size: var(--font-sm);
		color: #475569;
		line-height: 40rpx;
	}

	.mini-actions {
		margin-top: 16rpx;
		display: flex;
		justify-content: flex-start;
		flex-wrap: nowrap;
		gap: 12rpx;
	}
	.mini {
		flex: 1;
		min-width: 0;
		padding: 12rpx 10rpx;
		border-radius: 14rpx;
		background: rgba(255, 255, 255, 0.8);
		border: 1px solid rgba(15, 23, 42, 0.06);
		display: flex;
		align-items: center;
		justify-content: center;
		gap: 8rpx;
	}
	.mini:active {
		transform: scale(0.98);
	}
	.mini-text {
		font-size: var(--font-xs);
		color: #334155;
	}

	.card.disabled {
		opacity: 0.6;
	}

	.page.big {
		min-height: 100vh;
		padding: 12rpx 12rpx 20rpx;
		box-sizing: border-box;
		position: relative;
		overflow-x: hidden;
		overflow-y: auto;
		--neon-a: rgba(0, 240, 255, 0.95);
		--neon-b: rgba(60, 140, 255, 0.95);
		--neon-soft: rgba(0, 240, 255, 0.18);
		--glass: rgba(7, 13, 28, 0.42);
		--glass-2: rgba(10, 18, 38, 0.52);
		background: #020610;
		color: rgba(242, 252, 255, 0.96);
		font-size: var(--font-sm);
		line-height: 1.5;
		text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.55);
	}
	.big-center-media {
		width: 100%;
		height: calc(74vh - 360rpx);
		max-height: calc(720rpx + 7vh);
		border-radius: 18rpx;
		overflow: hidden;
	}
	.big-center-video {
		width: 100%;
		height: 100%;
		object-fit: cover;
		display: block;
	}
	.big-center-bg {
		width: 100%;
		height: 100%;
		border-radius: 18rpx;
		overflow: hidden;
		pointer-events: none;
		background-image: url('/static/bg.png');
		background-size: cover;
		background-position: center;
		background-repeat: no-repeat;
		opacity: 0.96;
		filter: saturate(1.08) contrast(1.08);
	}
	.big-center-text {
		border-radius: 18rpx;
		border: 1px solid rgba(0, 240, 255, 0.14);
		background: linear-gradient(180deg, rgba(7, 13, 28, 0.62) 0%, rgba(7, 13, 28, 0.46) 100%);
		box-shadow: 0 12rpx 24rpx rgba(0, 0, 0, 0.35);
		backdrop-filter: blur(6px);
		padding: 16rpx 18rpx;
	}
	.big-center-text-title {
		font-size: var(--font-base);
		font-weight: 900;
		color: rgba(220, 250, 255, 0.94);
		letter-spacing: 1rpx;
		text-shadow: 0 0 16rpx rgba(0, 240, 255, 0.16);
	}
	.big-center-text-desc {
		margin-top: 10rpx;
		font-size: var(--font-xs);
		line-height: 32rpx;
		color: rgba(201, 242, 255, 0.72);
	}
	.big-panel-inbox {
		margin-top: 16rpx;
		min-height: calc(320rpx + 2vh);
	}
	.big-panel-portrait .big-chart {
		height: calc(420rpx + 3vh);
	}
	.big-inbox-list {
		display: flex;
		flex-direction: column;
		gap: 12rpx;
	}
	.big-inbox-item {
		border-radius: 14rpx;
		padding: 14rpx 14rpx;
		border: 1px solid rgba(116, 216, 255, 0.18);
		background: rgba(7, 13, 28, 0.78);
		box-sizing: border-box;
	}
	.big-inbox-title {
		font-size: var(--font-sm);
		font-weight: 900;
		color: rgba(242, 252, 255, 0.92);
	}
	.big-inbox-desc {
		margin-top: 8rpx;
		font-size: var(--font-xs);
		color: rgba(242, 252, 255, 0.84);
		line-height: 28rpx;
		max-height: calc(56rpx + 0.5vh);
		overflow: hidden;
	}
	.page.big:before {
		content: '';
		position: absolute;
		inset: 0;
		pointer-events: none;
		z-index: 0;
		background-image:
			linear-gradient(0deg, rgba(0, 240, 255, 0.12) 1px, rgba(0, 0, 0, 0) 1px),
			linear-gradient(90deg, rgba(60, 140, 255, 0.10) 1px, rgba(0, 0, 0, 0) 1px),
			linear-gradient(135deg, rgba(0, 240, 255, 0.10) 0%, rgba(165, 90, 255, 0.10) 100%),
			radial-gradient(circle at 20% 18%, rgba(0, 240, 255, 0.18) 0%, rgba(0, 240, 255, 0) 46%),
			radial-gradient(circle at 82% 70%, rgba(165, 90, 255, 0.14) 0%, rgba(165, 90, 255, 0) 52%);
		background-size:
			34rpx 2rpx,
			2rpx 34rpx,
			cover,
			cover,
			cover;
		background-position:
			14rpx 12rpx,
			12rpx 14rpx,
			calc(100% - 14rpx) 12rpx,
			calc(100% - 12rpx) 14rpx,
			14rpx calc(100% - 12rpx),
			12rpx calc(100% - 14rpx),
			calc(100% - 14rpx) calc(100% - 12rpx),
			calc(100% - 12rpx) calc(100% - 14rpx);
		background-repeat: no-repeat;
		opacity: 0.65;
	}
	.page.big:after {
		content: '';
		position: absolute;
		left: 14rpx;
		right: 14rpx;
		bottom: 0;
		height: 1px;
		background: linear-gradient(90deg, rgba(0, 240, 255, 0) 0%, rgba(0, 240, 255, 0.45) 50%, rgba(0, 240, 255, 0) 100%);
		opacity: 0.75;
	}
	.big-top {
		height: auto;
		display: flex;
		align-items: center;
		justify-content: center;
		position: relative;
		padding: 12rpx 0;
	}
	.big-title {
		position: relative;
		z-index: 3;
		display: inline-flex;
		align-items: center;
		justify-content: center;
		font-size: var(--font-3xl);
		font-weight: 900;
		letter-spacing: 2rpx;
		color: rgba(242, 252, 255, 0.98);
		min-height: calc(120rpx + 1.2vh);
		width: 100%;
		padding: 0;
		box-sizing: border-box;
		text-shadow:
			0 2rpx 12rpx rgba(0, 0, 0, 0.65),
			0 0 18rpx rgba(0, 240, 255, 0.26),
			0 0 32rpx rgba(60, 140, 255, 0.18);
	}
	.big-title:before {
		content: '';
		position: absolute;
		inset: 0;
		background-image: url('/static/title.png');
		background-size: 100% 100%;
		background-repeat: no-repeat;
		background-position: center;
		pointer-events: none;
		z-index: 2;
	}
	.big-title-text {
		position: relative;
		z-index: 3;
		padding: 0 40rpx;
		box-sizing: border-box;
		font-size: calc(48rpx + 0.9vw);
	}
	.big-title {
		min-height: calc(120rpx + 1.2vh);
		width: 100%;
		padding: 0;
	}
	.big-title i {
		display: none;
	}
	.big-top-actions {
		position: relative;
		z-index: 1;
		margin-top: 12rpx;
		border-radius: 14rpx;
		border: 1px solid rgba(0, 240, 255, 0.18);
		background: linear-gradient(180deg, rgba(2, 10, 26, 0.88) 0%, rgba(2, 10, 26, 0.70) 100%);
		box-shadow: 0 10rpx 22rpx rgba(0, 0, 0, 0.40);
		backdrop-filter: blur(10px);
		overflow: hidden;
		padding: 10rpx 10rpx;
	}
	.big-top-action-row {
		display: grid;
		grid-template-columns: repeat(6, minmax(0, 1fr));
		gap: 12rpx;
		align-items: center;
	}
	.big-top-action-item {
		height: calc(140rpx + 1.5vh);
		display: flex;
		align-items: center;
		justify-content: center;
		border-radius: 14rpx;
		border: 1px solid rgba(116, 216, 255, 0.16);
		background: rgba(7, 13, 28, 0.38);
		box-sizing: border-box;
	}
	.big-top-action-ico {
		width: calc(124rpx + 1.2vh);
		height: calc(124rpx + 1.2vh);
		filter: drop-shadow(0 0 14rpx rgba(0, 240, 255, 0.26)) drop-shadow(0 0 26rpx rgba(60, 140, 255, 0.18));
	}
	.big-grid {
		margin-top: 14rpx;
		display: flex;
		gap: 12rpx;
		width: 100%;
		max-width: none;
		margin-left: 0;
		margin-right: 0;
		justify-content: space-between;
	}
	.big-col {
		width: 27%;
		display: flex;
		flex-direction: column;
		gap: 16rpx;
	}
	.big-center {
		flex: 0 0 46%;
		width: 46%;
		min-width: 0;
		display: flex;
		flex-direction: column;
		gap: 16rpx;
	}
	.big-panel {
		position: relative;
		border-radius: 18rpx;
		padding: 18rpx 18rpx 16rpx;
		min-height: calc(320rpx + 2vh);
		border: 1px solid rgba(0, 240, 255, 0.16);
		background-image:
			linear-gradient(135deg, rgba(0, 240, 255, 0.08) 0%, rgba(60, 140, 255, 0.05) 35%, rgba(165, 90, 255, 0.06) 100%),
			linear-gradient(180deg, rgba(7, 13, 28, 0.72) 0%, rgba(7, 13, 28, 0.56) 100%);
		background-repeat: no-repeat;
		background-size: cover;
		backdrop-filter: blur(6px);
		box-shadow: 0 14rpx 26rpx rgba(0, 0, 0, 0.42);
		overflow: hidden;
	}
	.big-panel-core {
		min-height: calc(460rpx + 3vh);
	}
	.big-panel-core .big-ring {
		height: calc(440rpx + 3vh);
	}
	.big-panel-core .big-ring:before {
		width: calc(440rpx + 3vh);
		height: calc(440rpx + 3vh);
	}
	.big-panel-core .big-ring-center {
		width: calc(250rpx + 2vh);
		height: calc(250rpx + 2vh);
	}
	.big-metrics {
		position: relative;
		z-index: 3;
		display: flex;
		align-items: flex-end;
		justify-content: center;
		padding: 8rpx 0 16rpx;
		margin-bottom: 8rpx;
	}
	.big-metric {
		text-align: center;
		min-width: 340rpx;
	}
	.big-metric-label {
		font-size: var(--font-sm);
		font-weight: 700;
		color: rgba(220, 250, 255, 0.78);
		letter-spacing: 1rpx;
		margin-bottom: 6rpx;
	}
	.big-metric-value {
		display: block;
		font-size: var(--font-2xl);
		font-weight: 900;
		line-height: 1;
		color: rgba(242, 252, 255, 0.96);
		text-shadow:
			0 10rpx 28rpx rgba(0, 0, 0, 0.55),
			0 0 18rpx rgba(0, 240, 255, 0.22);
	}
	.big-panel-core .big-ring {
		margin-top: 4rpx;
	}
	.big-panel:before {
		content: '';
		position: absolute;
		inset: 0;
		pointer-events: none;
		z-index: 0;
		background-image:
			linear-gradient(90deg, rgba(0,240,255,0.65), rgba(0,240,255,0)),
			linear-gradient(180deg, rgba(0,240,255,0.65), rgba(0,240,255,0)),
			linear-gradient(270deg, rgba(0,240,255,0.65), rgba(0,240,255,0)),
			linear-gradient(180deg, rgba(0,240,255,0.65), rgba(0,240,255,0)),
			linear-gradient(90deg, rgba(60,140,255,0.35), rgba(60,140,255,0)),
			linear-gradient(0deg, rgba(60,140,255,0.35), rgba(60,140,255,0)),
			linear-gradient(270deg, rgba(60,140,255,0.35), rgba(60,140,255,0)),
			linear-gradient(0deg, rgba(60,140,255,0.35), rgba(60,140,255,0));
		background-size:
			34rpx 2rpx,
			2rpx 34rpx,
			34rpx 2rpx,
			2rpx 34rpx,
			34rpx 2rpx,
			2rpx 34rpx,
			34rpx 2rpx,
			2rpx 34rpx;
		background-position:
			14rpx 12rpx,
			12rpx 14rpx,
			calc(100% - 14rpx) 12rpx,
			calc(100% - 12rpx) 14rpx,
			14rpx calc(100% - 12rpx),
			12rpx calc(100% - 14rpx),
			calc(100% - 14rpx) calc(100% - 12rpx),
			calc(100% - 12rpx) calc(100% - 14rpx);
		background-repeat: no-repeat;
		opacity: 0.65;
	}
	.big-panel:after {
		content: '';
		position: absolute;
		left: 14rpx;
		right: 14rpx;
		bottom: 0;
		height: 1px;
		background: linear-gradient(90deg, rgba(0,240,255,0) 0%, rgba(0,240,255,0.45) 50%, rgba(0,240,255,0) 100%);
		opacity: 0.75;
	}
	.big-panel-head {
		position: relative;
		z-index: 1;
		display: flex;
		align-items: flex-end;
		justify-content: space-between;
		margin-bottom: 14rpx;
	}
	.big-panel-head:after {
		content: '';
		position: absolute;
		left: 0;
		right: 0;
		bottom: -10rpx;
		height: 1px;
		background: linear-gradient(90deg, rgba(0,240,255,0) 0%, rgba(0,240,255,0.22) 18%, rgba(0,240,255,0.55) 50%, rgba(0,240,255,0.22) 82%, rgba(0,240,255,0) 100%);
		opacity: 0.95;
	}
	.big-panel-title {
		font-size: var(--font-lg);
		font-weight: 900;
		color: rgba(242, 252, 255, 0.94);
		padding-left: 14rpx;
		letter-spacing: 1rpx;
	}
	.big-panel-report .big-panel-head {
		margin-bottom: 18rpx;
		padding: 8rpx 14rpx 12rpx;
		border-radius: 14rpx;
		background: linear-gradient(90deg, rgba(0, 240, 255, 0.12) 0%, rgba(2, 8, 22, 0.18) 45%, rgba(165, 90, 255, 0.10) 100%);
		border: 1px solid rgba(0, 240, 255, 0.18);
		box-shadow: 0 0 18rpx rgba(0, 240, 255, 0.10);
		overflow: hidden;
	}
	.big-panel-report .big-panel-head:after {
		bottom: 0;
		height: 2rpx;
		background: linear-gradient(90deg, rgba(0,240,255,0) 0%, rgba(0,240,255,0.65) 20%, rgba(165,90,255,0.55) 60%, rgba(0,240,255,0) 100%);
		opacity: 1;
	}
	.big-panel-report .big-panel-head:before {
		content: '';
		position: absolute;
		right: -40rpx;
		top: -50rpx;
		width: 160rpx;
		height: 160rpx;
		background: radial-gradient(circle at 30% 30%, rgba(0, 240, 255, 0.22) 0%, rgba(0, 240, 255, 0) 62%);
		transform: rotate(12deg);
		pointer-events: none;
		opacity: 0.9;
	}
	.big-panel-report .big-panel-title {
		padding-left: 20rpx;
		text-shadow:
			0 0 16rpx rgba(0, 240, 255, 0.22),
			0 2rpx 10rpx rgba(0, 0, 0, 0.55);
	}
	.big-panel-report .big-panel-title:before {
		left: 10rpx;
		top: 50%;
		transform: translateY(-50%);
		height: calc(22rpx + 0.8vh);
		background: linear-gradient(180deg, rgba(0, 240, 255, 0.95) 0%, rgba(39, 120, 255, 0.85) 55%, rgba(165, 90, 255, 0.85) 100%);
		box-shadow: 0 0 14rpx rgba(0, 240, 255, 0.18);
	}
	.big-panel-portrait {
		border-color: transparent;
		box-shadow: none;
	}
	.big-panel-portrait:before,
	.big-panel-portrait:after {
		opacity: 0;
	}
	.big-panel-portrait .big-panel-head:after {
		opacity: 0;
	}
	.big-panel-portrait .big-panel-title:before {
		opacity: 0;
	}
	.big-panel-title:before {
		content: '';
		position: absolute;
		left: 0;
		top: 50%;
		transform: translateY(-50%);
		width: 6rpx;
		height: calc(24rpx + 0.8vh);
		border-radius: 6rpx;
		background: linear-gradient(180deg, rgba(116, 216, 255, 0.95) 0%, rgba(43, 107, 255, 0.85) 100%);
	}
	.big-panel-sub {
		font-size: var(--font-sm);
		color: rgba(242, 252, 255, 0.84);
		font-weight: 800;
	}
	.big-ai-head-actions {
		display: flex;
		align-items: center;
		gap: 14rpx;
	}
	.big-panel-ai.is-collapsed {
		padding-bottom: 10rpx;
	}
	.big-panel-ai.is-open {
		padding-bottom: 22rpx;
	}
	.big-panel-clear {
		padding: 6rpx 12rpx;
		border-radius: 12rpx;
		font-size: var(--font-xs);
		font-weight: 900;
		color: rgba(242, 252, 255, 0.86);
		border: 1px solid rgba(116, 216, 255, 0.20);
		background: rgba(7, 13, 28, 0.45);
		box-sizing: border-box;
	}
	.big-panel-clear:active {
		opacity: 0.85;
	}
	.big-panel-ai {
		margin-top: 14rpx;
	}
	.big-panel-head-ai {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 12rpx;
	}
	.big-ai-body {
		position: relative;
		z-index: 1;
	}
	.big-ai-messages {
		height: calc(640rpx + 4vh);
		padding: 8rpx 10rpx;
		box-sizing: border-box;
		border-radius: 14rpx;
		border: 1px solid rgba(116, 216, 255, 0.14);
		background: rgba(7, 13, 28, 0.55);
	}
	.big-ai-empty {
		padding: 16rpx 10rpx;
		font-size: var(--font-xs);
		color: rgba(242, 252, 255, 0.62);
		font-weight: 700;
		line-height: 1.4;
	}
	.big-ai-msg {
		display: flex;
		margin: 10rpx 0;
	}
	.big-ai-msg.is-user {
		justify-content: flex-end;
	}
	.big-ai-msg.is-ai {
		justify-content: flex-start;
	}
	.big-ai-bubble {
		max-width: 78%;
		padding: 12rpx 14rpx;
		border-radius: 14rpx;
		font-size: var(--font-sm);
		line-height: 1.45;
		color: rgba(242, 252, 255, 0.92);
		border: 1px solid rgba(0, 240, 255, 0.16);
		background: linear-gradient(180deg, rgba(0, 188, 255, 0.10) 0%, rgba(2, 8, 22, 0.82) 70%, rgba(2, 8, 22, 0.92) 100%);
		box-shadow: 0 0 16rpx rgba(0, 166, 255, 0.08);
		word-break: break-all;
	}
	.big-ai-msg.is-user .big-ai-bubble {
		border-color: rgba(43, 107, 255, 0.22);
		background: linear-gradient(180deg, rgba(43, 107, 255, 0.16) 0%, rgba(2, 8, 22, 0.82) 70%, rgba(2, 8, 22, 0.92) 100%);
	}
	.big-ai-bottom {
		height: 1px;
	}
	.big-ai-input {
		margin-top: 12rpx;
		display: flex;
		align-items: center;
		gap: 10rpx;
	}
	.big-ai-voice-btn {
		width: calc(72rpx + 2vh);
		height: calc(72rpx + 2vh);
		border-radius: 14rpx;
		display: flex;
		align-items: center;
		justify-content: center;
		border: 1px solid rgba(116, 216, 255, 0.18);
		background: rgba(7, 13, 28, 0.78);
		transition: all 0.3s ease;
	}
	.big-ai-voice-btn.active {
		border-color: rgba(0, 240, 255, 0.6);
		background: rgba(0, 240, 255, 0.12);
		box-shadow: 0 0 20rpx rgba(0, 240, 255, 0.4);
	}
	.big-ai-text {
		flex: 1;
		height: calc(72rpx + 2vh);
		border-radius: 14rpx;
		border: 1px solid rgba(116, 216, 255, 0.18);
		background: rgba(7, 13, 28, 0.78);
		padding: 0 14rpx;
		font-size: var(--font-sm);
		color: rgba(242, 252, 255, 0.92);
		box-sizing: border-box;
	}
	.big-ai-send {
		width: 120rpx;
		height: calc(72rpx + 2vh);
		border-radius: 14rpx;
		display: flex;
		align-items: center;
		justify-content: center;
		font-size: var(--font-sm);
		font-weight: 900;
		color: rgba(242, 252, 255, 0.92);
		border: 1px solid rgba(0, 240, 255, 0.22);
		background: linear-gradient(180deg, rgba(0, 240, 255, 0.16) 0%, rgba(2, 8, 22, 0.92) 100%);
		box-sizing: border-box;
	}
	.big-ai-send.disabled {
		opacity: 0.5;
	}
	.big-ring {
		position: relative;
		z-index: 1;
		height: calc(360rpx + 2.5vh);
	}
	.big-ring:before {
		content: '';
		position: absolute;
		left: 50%;
		top: 50%;
		transform: translate(-50%, -50%);
		width: calc(360rpx + 2.5vh);
		height: calc(360rpx + 2.5vh);
		border-radius: 999rpx;
		pointer-events: none;
		z-index: 0;
		background-image: url('/static/hud-ring.png');
		background-repeat: no-repeat;
		background-position: center;
		background-size: contain;
		opacity: 0.38;
		filter: drop-shadow(0 0 18rpx rgba(0, 240, 255, 0.22));
		animation: bigRingSpin 22s linear infinite;
	}
	@keyframes bigRingSpin {
		0% { transform: translate(-50%, -50%) rotate(0deg); }
		100% { transform: translate(-50%, -50%) rotate(360deg); }
	}
	.big-ring-center {
		position: absolute;
		left: 50%;
		top: 50%;
		transform: translate(-50%, -50%);
		width: calc(210rpx + 5vh);
		height: calc(210rpx + 5vh);
		border-radius: 999rpx;
		background: rgba(7, 13, 28, 0.72);
		z-index: 5;
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		gap: 6rpx;
		box-sizing: border-box;
	}
	.big-ring-center-main {
		font-size: var(--font-lg);
		font-weight: 900;
		color: rgba(242, 252, 255, 0.96);
	}
	.big-ring-center-sub {
		font-size: var(--font-xs);
		color: rgba(242, 252, 255, 0.84);
		text-align: center;
		line-height: 26rpx;
		padding: 0 12rpx;
	}
	.big-chart {
		position: relative;
		z-index: 1;
		height: calc(340rpx + 2.5vh);
	}
	.big-portrait-cloud {
		position: relative;
		width: 100%;
		height: 100%;
		overflow: hidden;
	}
	.big-portrait-word {
		position: absolute;
		left: 50%;
		top: 50%;
		font-weight: 800;
		letter-spacing: 1rpx;
		white-space: nowrap;
		text-shadow:
			0 2rpx 10rpx rgba(0, 0, 0, 0.55),
			0 0 14rpx rgba(0, 240, 255, 0.08);
		filter: saturate(1.08);
		will-change: transform;
	}
	@keyframes portraitFloat1 {
		0% { transform: translate(-50%, -50%) translate(0, 0); }
		50% { transform: translate(-50%, -50%) translate(0, -10rpx); }
		100% { transform: translate(-50%, -50%) translate(0, 0); }
	}
	@keyframes portraitFloat2 {
		0% { transform: translate(-50%, -50%) translate(0, 0); }
		50% { transform: translate(-50%, -50%) translate(10rpx, 0); }
		100% { transform: translate(-50%, -50%) translate(0, 0); }
	}
	@keyframes portraitFloat3 {
		0% { transform: translate(-50%, -50%) translate(0, 0); }
		50% { transform: translate(-50%, -50%) translate(-8rpx, 8rpx); }
		100% { transform: translate(-50%, -50%) translate(0, 0); }
	}
	@keyframes portraitFloat4 {
		0% { transform: translate(-50%, -50%) translate(0, 0); }
		50% { transform: translate(-50%, -50%) translate(8rpx, -6rpx); }
		100% { transform: translate(-50%, -50%) translate(0, 0); }
	}
	.big-chart-xl {
		height: calc(400rpx + 3vh);
	}
	.big-kpis {
		position: relative;
		z-index: 1;
		display: flex;
		flex-wrap: wrap;
		gap: 12rpx;
	}
	.big-kpi {
		width: calc(50% - 6rpx);
		border-radius: 14rpx;
		padding: 14rpx 12rpx;
		border: 1px solid rgba(116, 216, 255, 0.18);
		background: rgba(7, 13, 28, 0.78);
		box-sizing: border-box;
	}
	.big-kpi-num-row {
		display: flex;
		align-items: center;
		gap: 10rpx;
	}
	.big-kpi-ico {
		width: calc(34rpx + 1vh);
		height: calc(34rpx + 1vh);
		filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
	}
	.big-kpi-num {
		font-size: var(--font-xl);
		font-weight: 900;
		color: #74d8ff;
		text-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.65);
	}
	.big-kpi-lab {
		margin-top: 6rpx;
		font-size: var(--font-sm);
		color: rgba(242, 252, 255, 0.84);
		font-weight: 800;
	}
	.big-tools {
		position: relative;
		z-index: 1;
		display: flex;
		gap: 12rpx;
		margin-bottom: 10rpx;
	}
	.big-action-grid {
		display: grid;
		grid-template-columns: repeat(3, 1fr);
		gap: 18rpx;
		padding: 10rpx 2rpx 2rpx;
	}
	.big-panel-actions {
		min-height: calc(380rpx + 3vh);
	}
	.big-action-item {
		position: relative;
		z-index: 1;
		height: calc(150rpx + 1.5vh);
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		gap: 10rpx;
		padding: 14rpx 10rpx;
		border-radius: 16rpx;
		border: 1px solid rgba(0, 188, 255, 0.22);
		background: linear-gradient(180deg, rgba(0, 188, 255, 0.10) 0%, rgba(2, 8, 22, 0.82) 70%, rgba(2, 8, 22, 0.92) 100%);
		box-shadow: 0 0 18rpx rgba(0, 166, 255, 0.10);
		box-sizing: border-box;
		overflow: hidden;
	}
	.big-action-item:active { transform: scale(0.98); }
	.big-action-ico {
		width: calc(68rpx + 2vh);
		height: calc(68rpx + 2vh);
		flex: 0 0 auto;
		filter: drop-shadow(0 0 10rpx rgba(0, 200, 255, 0.22));
	}
	.big-action-text {
		font-size: var(--font-base);
		font-weight: 900;
		color: rgba(242, 252, 255, 0.92);
		line-height: 1.1;
		text-align: center;
	}
	.big-action-item.c-blue { border-color: rgba(0, 188, 255, 0.40); box-shadow: 0 0 20rpx rgba(0, 188, 255, 0.14); }
	.big-action-item.c-blue2 { border-color: rgba(39, 120, 255, 0.40); box-shadow: 0 0 20rpx rgba(39, 120, 255, 0.14); }
	.big-action-item.c-red { border-color: rgba(255, 72, 92, 0.40); box-shadow: 0 0 20rpx rgba(255, 72, 92, 0.12); }
	.big-action-item.c-cyan { border-color: rgba(0, 240, 255, 0.40); box-shadow: 0 0 20rpx rgba(0, 240, 255, 0.12); }
	.big-action-item.c-yellow { border-color: rgba(255, 196, 45, 0.40); box-shadow: 0 0 20rpx rgba(255, 196, 45, 0.12); }
	.big-action-item.c-purple { border-color: rgba(165, 90, 255, 0.40); box-shadow: 0 0 20rpx rgba(165, 90, 255, 0.12); }
	.big-tool {
		flex: 1;
		border-radius: 14rpx;
		border: 1px solid rgba(116, 216, 255, 0.18);
		background: rgba(7, 13, 28, 0.78);
		padding: 12rpx 12rpx;
		display: flex;
		align-items: center;
		gap: 10rpx;
	}
	.big-tool-icon {
		width: calc(50rpx + 1.5vh);
		height: calc(50rpx + 1.5vh);
		border-radius: 12rpx;
		border: 1px solid rgba(116, 216, 255, 0.22);
		background: rgba(10, 18, 38, 0.55);
		display: flex;
		align-items: center;
		justify-content: center;
	}
	.big-tool-text {
		font-size: var(--font-sm);
		font-weight: 900;
		color: rgba(242, 252, 255, 0.92);
	}
	.big-nav {
		display: flex;
		flex-wrap: wrap;
		gap: 12rpx;
	}
	.big-nav-item {
		width: calc(33.33% - 8rpx);
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		gap: 8rpx;
		padding: 10rpx 0;
	}
	.big-hex {
		width: calc(96rpx + 3vh);
		height: calc(86rpx + 2.7vh);
		clip-path: polygon(25% 6.7%, 75% 6.7%, 100% 50%, 75% 93.3%, 25% 93.3%, 0% 50%);
		border: 1px solid rgba(116, 216, 255, 0.28);
		background: radial-gradient(circle at 50% 30%, rgba(116, 216, 255, 0.22) 0%, rgba(10, 18, 38, 0.55) 70%);
		display: flex;
		align-items: center;
		justify-content: center;
		box-shadow: 0 0 18rpx rgba(116, 216, 255, 0.18);
	}
	.big-nav-text {
		font-size: var(--font-xs);
		font-weight: 900;
		color: rgba(242, 252, 255, 0.86);
	}
	.hex-img { display: none; }

	@media (max-width: 980px) {
		.big-grid {
			flex-direction: column;
		}
		.big-col {
			width: 100%;
		}
	}
	
	/* 登录弹窗显示时禁用页面交互 */
	.page-disabled {
		pointer-events: none;
		user-select: none;
	}
</style>
