273 lines
16 KiB
HTML
273 lines
16 KiB
HTML
@if (auth.isAuthenticated()) {
|
||
<div class="mobile-app-shell">
|
||
<!-- Top App Bar -->
|
||
<header class="top-bar">
|
||
<div class="logo">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary" style="color: var(--color-primary-dark)"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
||
<span class="logo-text">深维面诊</span>
|
||
</div>
|
||
|
||
<div style="flex: 1"></div>
|
||
|
||
<!-- Fullscreen Toggle -->
|
||
<button class="fullscreen-btn" (click)="toggleFullscreen()">
|
||
<svg *ngIf="!isFullscreen" xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.707l4.096-4.096a.5.5 0 0 0 0-.707m4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.793l-4.096-4.096a.5.5 0 0 1 0-.707"/>
|
||
<path fill-rule="evenodd" d="M10.172 5.828a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.793l-4.096 4.096a.5.5 0 0 0 0 .707m-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.707l4.096 4.096a.5.5 0 0 1 0 .707"/>
|
||
</svg>
|
||
<svg *ngIf="isFullscreen" xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.707l4.096-4.096a.5.5 0 0 0 0-.707m4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.793l-4.096-4.096a.5.5 0 0 1 0-.707"/>
|
||
<path fill-rule="evenodd" d="M10.172 5.828a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.793l-4.096 4.096a.5.5 0 0 0 0 .707m-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.707l4.096 4.096a.5.5 0 0 1 0 .707"/>
|
||
</svg>
|
||
{{ isFullscreen ? '退出全屏' : '全屏预览' }}
|
||
</button>
|
||
|
||
<!-- Profile Avatar triggers billing modal -->
|
||
<button class="avatar-btn" (click)="showBillingModal = true">
|
||
<div class="avatar-circle">
|
||
@if (auth.user()?.avatarUrl) {
|
||
<img [src]="auth.user()?.avatarUrl" alt="Avatar" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">
|
||
} @else {
|
||
{{ auth.user()?.name?.charAt(0) || auth.user()?.phone?.slice(-4, -3) || '我' }}
|
||
}
|
||
</div>
|
||
</button>
|
||
</header>
|
||
|
||
<!-- Scrollable Main Content -->
|
||
<main class="main-content">
|
||
<router-outlet></router-outlet>
|
||
</main>
|
||
|
||
<!-- Capsule Bottom Navigation (Like Get Notes app) -->
|
||
<nav class="pill-nav-container" *ngIf="!isContextPage">
|
||
<div class="pill-nav">
|
||
<!-- Nav Item 1 -->
|
||
<a routerLink="/workspace" routerLinkActive="active" class="pill-item">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>
|
||
<span>笔记列表</span>
|
||
</a>
|
||
|
||
<!-- Central Record Bubble -->
|
||
<div class="pill-action">
|
||
<button class="mic-bubble"
|
||
(mousedown)="onPressStart($event)" (touchstart)="onPressStart($event)"
|
||
(mouseup)="onPressEnd($event)" (touchend)="onPressEnd($event)"
|
||
(mouseleave)="onPressEnd($event)" (touchcancel)="onPressEnd($event)">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="22"></line></svg>
|
||
<span style="user-select: none;">面诊</span>
|
||
</button>
|
||
<!-- Hidden input mapped to long press -->
|
||
<input type="file" #fileInput hidden (change)="handleFileUpload($event)" accept="audio/*">
|
||
</div>
|
||
|
||
<!-- Nav Item 2 -->
|
||
<a routerLink="/clients" routerLinkActive="active" class="pill-item">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||
<span>客户通</span>
|
||
</a>
|
||
</div>
|
||
|
||
<div class="pill-tip">短按开始,长按上传</div>
|
||
</nav>
|
||
|
||
<!-- Report/Context Page Chat Pill -->
|
||
<div class="chat-pill-wrapper" *ngIf="isContextPage && !isContextGenerating" (click)="openChatSheet()">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="2" style="margin-top:2px"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>
|
||
<span class="placeholder">{{ chatPlaceholder }}</span>
|
||
<button class="send-btn-mock">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Global Action Sheet for Recording, Processing, & Chat -->
|
||
<div class="action-sheet-overlay" [class.show]="showActionSheet || showChatSheet" (click)="closeSheets()">
|
||
<div class="action-sheet-panel" [class.show]="showActionSheet || showChatSheet" (click)="$event.stopPropagation()">
|
||
<div class="sheet-drag-handle"></div>
|
||
|
||
<!-- STATE 1: Actively Recording (only when showActionSheet is true) -->
|
||
<ng-container *ngIf="showActionSheet">
|
||
<div class="recording-state" *ngIf="isRecording">
|
||
<div class="rec-timer">
|
||
<div class="red-dot animate-pulse"></div>
|
||
{{ formatDuration(recordDuration) }}
|
||
</div>
|
||
<p class="rec-subtitle">深维云脑:正在实时接听面诊中...</p>
|
||
|
||
<button class="stop-btn" (click)="stopAndProcess()">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2" ry="2"></rect></svg>
|
||
结束对谈 · 生成绝杀 X光片
|
||
</button>
|
||
</div>
|
||
<!-- STATE 2: Processing (The Relay Bar) -->
|
||
<div class="processing-state" *ngIf="isProcessing">
|
||
<h3 class="process-title" *ngIf="!uploadComplete">🧠 深维专家团并行推理中...</h3>
|
||
<h3 class="process-title text-success" *ngIf="uploadComplete">✅ X光片就绪</h3>
|
||
|
||
<div class="relay-nodes">
|
||
@for (agent of agents; track agent.id; let i = $index) {
|
||
<div class="agent-node" [class.active]="agent.status === 'processing'" [class.done]="agent.status === 'completed'">
|
||
<div class="node-icon-wrapper">
|
||
<span class="node-icon" [innerHTML]="agent.iconHtml"></span>
|
||
<div class="pulse-ring" *ngIf="agent.status === 'processing'"></div>
|
||
</div>
|
||
<span class="node-name">{{ agent.name }}</span>
|
||
<div class="connector" *ngIf="i < agents.length - 1" [class.active]="agent.status === 'completed'"></div>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<p class="cost-hint text-center mt-2" *ngIf="!uploadComplete">预估消耗 1000 算力点数</p>
|
||
</div>
|
||
</ng-container>
|
||
|
||
<!-- STATE 3: Chat Sheet -->
|
||
<div class="chat-state" *ngIf="showChatSheet" style="display:flex; flex-direction:column; height: 50vh;">
|
||
<div class="chat-header">
|
||
<h4>💬 X光片追踪深打</h4>
|
||
<p *ngIf="chatCtx.contextType() === 'recording'">🎙️ 上下文锁定:本次面诊录音报告</p>
|
||
<p *ngIf="chatCtx.contextType() === 'client'">👤 上下文锁定:{{ chatCtx.contextTitle() }} 全景档案</p>
|
||
<p *ngIf="!chatCtx.contextType()">上下文锁定</p>
|
||
</div>
|
||
|
||
<div class="chat-history">
|
||
@for (msg of chatMessages; track $index) {
|
||
<div class="chat-message" [class.user]="msg.role === 'user'">
|
||
|
||
<!-- AI 头像:方形 -->
|
||
<div class="msg-avatar ai-avatar" *ngIf="msg.role !== 'user'">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||
</div>
|
||
|
||
<div class="msg-bubble" [class.streaming]="msg.isStreaming" [innerHTML]="msg.content"></div>
|
||
|
||
<!-- 用户头像:圆形 -->
|
||
<div class="msg-avatar user-avatar" *ngIf="msg.role === 'user'">
|
||
{{ auth.user()?.name?.charAt(0) || '我' }}
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<div class="chat-input-area">
|
||
<input type="text" [(ngModel)]="chatInput" placeholder="输入追问或索要话术..." (keyup.enter)="sendChatMessage()">
|
||
<button class="chat-send-btn" (click)="sendChatMessage()" [disabled]="!chatInput.trim()">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dual-Tab Modal Overlay -->
|
||
<div class="modal-overlay" *ngIf="showBillingModal" (click)="showBillingModal = false" [class.show]="showBillingModal">
|
||
<div class="billing-modal" (click)="$event.stopPropagation()">
|
||
<div class="modal-header multi-tab">
|
||
<div class="tab-selectors">
|
||
<button class="modal-tab-btn" [class.active]="activeModalTab === 'billing'" (click)="activeModalTab = 'billing'">账户中心</button>
|
||
<button class="modal-tab-btn" [class.active]="activeModalTab === 'profile'" (click)="activeModalTab = 'profile'">个人档案</button>
|
||
</div>
|
||
<button class="close-btn" (click)="showBillingModal = false">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Tab 1: Billing -->
|
||
<div class="modal-body" *ngIf="activeModalTab === 'billing'">
|
||
<div class="points-card">
|
||
<div class="subtitle">剩余可用总点数</div>
|
||
<div class="points-val">{{ formatCredits(credits) }}</div>
|
||
</div>
|
||
<div class="quota-blocks">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" style="width: 30%"></div>
|
||
</div>
|
||
<div class="free-quota mt-2">今日免费评估额度: 300 / 1000</div>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="btn btn-outline" style="flex: 1;">点数订阅包</button>
|
||
<button class="btn btn-primary" style="flex: 1;" (click)="showBillingModal = false">充值点数</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab 2: Profile -->
|
||
<div class="modal-body profile-body" *ngIf="activeModalTab === 'profile'">
|
||
<div class="profile-form-group">
|
||
<label>系统头像</label>
|
||
<div class="avatar-edit-wrapper">
|
||
<div class="avatar-large">
|
||
@if (auth.user()?.avatarUrl) {
|
||
<img [src]="auth.user()?.avatarUrl" style="width:100%;height:100%;border-radius:50%;object-fit:cover" alt="avatar">
|
||
} @else {
|
||
{{ auth.user()?.name?.charAt(0) || auth.user()?.phone?.slice(-4, -3) || '我' }}
|
||
}
|
||
</div>
|
||
<div class="avatar-hint">头像是您的深维体系全局身份标识<br>暂不支持在此修改,请前往主系统配置</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="profile-row">
|
||
<div class="profile-form-group" style="flex: 1">
|
||
<label>真实姓名</label>
|
||
<input type="text" class="profile-input" [(ngModel)]="profileRealName" placeholder="如: 李大夫">
|
||
</div>
|
||
<div class="profile-form-group" style="flex: 1">
|
||
<label>核心角色</label>
|
||
<div class="role-pills">
|
||
<button class="role-pill" [class.active]="profileRole === 'doctor'" (click)="profileRole = 'doctor'">医生</button>
|
||
<button class="role-pill" [class.active]="profileRole === 'assistant'" (click)="profileRole = 'assistant'">医助</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="profile-form-group">
|
||
<label>所属机构名称 (公司名)</label>
|
||
<input type="text" class="profile-input" [(ngModel)]="profileCompany" placeholder="请输入您的执业机构全称">
|
||
</div>
|
||
|
||
<div class="profile-form-group">
|
||
<label>防干扰专属声纹特征 <span class="badge-pro" style="transform:scale(0.8);transform-origin:left;">Pro专用</span></label>
|
||
<p style="font-size: 13px; color: var(--color-slate-500); margin: 0 0 12px 0;">系统会通过声纹锁定主诊人,屏蔽杂音。请录制一段10秒左右的本人说话声音:</p>
|
||
|
||
<div class="voice-box">
|
||
<div *ngIf="profileVoiceUrl" class="voice-ready">
|
||
<svg class="text-success" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:6px"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
|
||
<span style="flex:1">已登记个人声纹档案</span>
|
||
<button class="btn-text-danger" (click)="deleteProfileVoice()">删除重建</button>
|
||
</div>
|
||
|
||
<div *ngIf="!profileVoiceUrl && !isProfileRecording && !profileVoiceUploading" class="voice-record-btn" (click)="toggleProfileVoiceRecord()">
|
||
<svg style="margin-right:8px" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="22"></line></svg>
|
||
点击开始采集(推荐朗读上方文字)
|
||
</div>
|
||
|
||
<div *ngIf="isProfileRecording" class="voice-recording-btn with-progress" (click)="toggleProfileVoiceRecord()">
|
||
<div class="progress-fill-bg" [style.width.%]="recordProgress > 100 ? 100 : recordProgress" [style.background-color]="getRecordColor()"></div>
|
||
<div class="content-relative">
|
||
<div class="red-dot"></div>
|
||
正在聆听... {{ (recordProgress / 10).toFixed(1) }}s / 10s (点击停止)
|
||
</div>
|
||
</div>
|
||
|
||
<div *ngIf="profileVoiceUploading" class="voice-loading">
|
||
<div class="spinner"></div>正在提取波形并上传特征至云端...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions" style="margin-top: 24px">
|
||
<button class="btn btn-primary" style="flex: 1;" (click)="saveProfile()">
|
||
{{ profileSaveSuccess ? '✅ 保存成功' : '保存档案' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
} @else {
|
||
<app-login />
|
||
}
|
||
|