Files
DSHWatch/src/pages/index/index.ux
T

1100 lines
33 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="page">
<swiper class="swiper" index="{{currentIndex}}" indicator="false" loop="false"
previousmargin="0px" nextmargin="0px" onchange="onSwipeChange">
<!-- 最左页:选择对话 / 工作区 -->
<div class="page-item left-page">
<text class="page-title">选择对话</text>
<input class="new-session" type="button" value="+ 新会话" onclick="newSession" />
<text class="page-title small">工作区</text>
<div class="ws-group" for="{{workTree}}">
<text class="ws-name">📁 {{$item.name}}</text>
<div class="sel-conv {{$item.title === currentConv ? 'sel-active' : ''}}" for="{{$item.convs}}" onclick="cycleConv">
<text class="sel-title fl{{fontLevel}}">{{$item.title}}</text>
<text class="sel-time">{{$item.time}}</text>
</div>
</div>
</div>
<!-- 左页:对话(Harness 式) -->
<div class="page-item left-page">
<!-- 当前会话 / 点开选工作区 -->
<text class="page-title" onclick="toggleSelect">对话 · {{currentConv}}</text>
<!-- 工作区/会话选择器 -->
<div class="select-overlay" if="{{showSelect}}">
<div class="select-ws" for="{{workspaces}}" onclick="toggleSelect">
<text class="select-name">{{$item.name}}</text>
<div class="select-conv" for="{{$item.conversations}}" onclick="selectConversation($index,$item)">
<text class="select-conv-text">{{$item}}</text>
</div>
</div>
</div>
<!-- 消息流 -->
<scroll class="chat-scroll" scroll-y="true">
<div class="msg-row {{$item.role === 'user' ? 'row-user' : 'row-assistant'}}" for="{{messages}}">
<div class="chat-msg {{$item.role === 'user' ? 'msg-user' : 'msg-assistant'}}">
<div class="think-chip" if="{{$item.think}}">
<text>💭 思考…</text>
</div>
<text class="think-text fl{{fontLevel}}" if="{{$item.showThink}}">{{$item.think}}</text>
<text class="chat-text fl{{fontLevel}}">{{$item.collapsed ? $item.preview : $item.text}}</text>
<text class="expand-chip" if="{{$item.role === 'assistant' && $item.collapsed}}">展开 ▾</text>
<text class="expand-chip" if="{{$item.role === 'assistant' && !$item.collapsed}}">收起 ▴</text>
<image class="chat-img" src="/common/dsniang.png" if="{{$item.kind === 'image'}}" />
<div class="approve-box" if="{{$item.kind === 'approval'}}">
<input class="approve-btn allow" type="button" value="允许" />
<input class="approve-btn deny" type="button" value="拒绝" />
</div>
</div>
</div>
</scroll>
<!-- 底部输入栏 -->
<div class="input-bar">
<input class="input-field" type="text" value="{{inputText}}" placeholder="说点什么…" onclick="openInput" />
<input class="input-send" type="button" value="输入" onclick="openInput" />
</div>
</div>
<!-- 中页:主页 / 吉祥物(无按钮,点击互动) -->
<div class="page-item center-page">
<!-- 气泡区:始终占位,让气泡在头像正上方居中;只显示趣味/上轮消耗信息 -->
<div class="bubble-zone">
<div class="bubble" if="{{bubbleVisible}}">
<text class="bubble-text">{{bubbleText}}</text>
</div>
</div>
<image class="mascot step{{animStep}}" src="/common/dsniang.png" onclick="onTap" />
<text class="peak {{isPeak ? 'peak-red' : 'peak-green'}}">{{peakLabel}}</text>
<text class="stats">余额 {{balance}}</text>
<text class="stats">今日 {{todayTokens}} tokens · 预估 ¥{{todayCost}}</text>
</div>
<!-- 右页:丰富设置 -->
<div class="page-item right-page">
<text class="page-title">设置</text>
<div class="setting-row" onclick="toggleModel">
<text class="setting-label fl{{fontLevel}}">模型</text>
<text class="setting-value fl{{fontLevel}}">{{modelValue}}</text>
</div>
<div class="setting-row" onclick="toggleProvider">
<text class="setting-label fl{{fontLevel}}">提供方</text>
<text class="setting-value fl{{fontLevel}}">{{providerValue}}</text>
</div>
<div class="setting-row" onclick="toggleReason">
<text class="setting-label fl{{fontLevel}}">推理强度</text>
<text class="setting-value fl{{fontLevel}}">{{reasonValue}}</text>
</div>
<div class="setting-row" onclick="togglePerm">
<text class="setting-label fl{{fontLevel}}">权限模式</text>
<text class="setting-value fl{{fontLevel}}">{{permValue}}</text>
</div>
<div class="setting-row" onclick="toggleDialog">
<text class="setting-label fl{{fontLevel}}">对话模式</text>
<text class="setting-value fl{{fontLevel}}">{{dialogModeValue}}</text>
</div>
<text class="section-title fl{{fontLevel}}">手表端设置</text>
<div class="setting-row" onclick="toggleWatchConn">
<text class="setting-label fl{{fontLevel}}">手环连接</text>
<text class="setting-value fl{{fontLevel}}">{{watchConnValue}}</text>
</div>
<div class="setting-row" onclick="toggleServer">
<text class="setting-label fl{{fontLevel}}">服务器</text>
<text class="setting-value fl{{fontLevel}}">{{serverValue}}</text>
</div>
<div class="setting-row" onclick="openFontPicker">
<text class="setting-label fl{{fontLevel}}">字体大小</text>
<text class="setting-value fl{{fontLevel}}">{{fontLabel}}</text>
</div>
<text class="conn-text">{{connText}}</text>
</div>
</swiper>
<div class="dots">
<div class="dot {{currentIndex == 0 ? 'dot-active' : ''}}"></div>
<div class="dot {{currentIndex == 1 ? 'dot-active' : ''}}"></div>
<div class="dot {{currentIndex == 2 ? 'dot-active' : ''}}"></div>
<div class="dot {{currentIndex == 3 ? 'dot-active' : ''}}"></div>
</div>
<!-- 软键盘弹层:输入框上移、键盘贴底 -->
<div class="keyboard" if="{{showKeyboard}}">
<div class="kb-input">
<text class="kb-text">{{inputText}}</text>
</div>
<div class="kb-keys">
<div class="kb-row">
<text class="kb-key" onclick="kbKey('q')">q</text><text class="kb-key" onclick="kbKey('w')">w</text><text class="kb-key" onclick="kbKey('e')">e</text><text class="kb-key" onclick="kbKey('r')">r</text><text class="kb-key" onclick="kbKey('t')">t</text><text class="kb-key" onclick="kbKey('y')">y</text><text class="kb-key" onclick="kbKey('u')">u</text><text class="kb-key" onclick="kbKey('i')">i</text><text class="kb-key" onclick="kbKey('o')">o</text><text class="kb-key" onclick="kbKey('p')">p</text>
</div>
<div class="kb-row">
<text class="kb-key" onclick="kbKey('a')">a</text><text class="kb-key" onclick="kbKey('s')">s</text><text class="kb-key" onclick="kbKey('d')">d</text><text class="kb-key" onclick="kbKey('f')">f</text><text class="kb-key" onclick="kbKey('g')">g</text><text class="kb-key" onclick="kbKey('h')">h</text><text class="kb-key" onclick="kbKey('j')">j</text><text class="kb-key" onclick="kbKey('k')">k</text><text class="kb-key" onclick="kbKey('l')">l</text>
</div>
<div class="kb-row">
<text class="kb-key" onclick="kbKey('z')">z</text><text class="kb-key" onclick="kbKey('x')">x</text><text class="kb-key" onclick="kbKey('c')">c</text><text class="kb-key" onclick="kbKey('v')">v</text><text class="kb-key" onclick="kbKey('b')">b</text><text class="kb-key" onclick="kbKey('n')">n</text><text class="kb-key" onclick="kbKey('m')">m</text>
</div>
<div class="kb-row">
<text class="kb-key" onclick="kbKey('1')">1</text><text class="kb-key" onclick="kbKey('2')">2</text><text class="kb-key" onclick="kbKey('3')">3</text><text class="kb-key" onclick="kbKey('4')">4</text><text class="kb-key" onclick="kbKey('5')">5</text><text class="kb-key" onclick="kbKey('6')">6</text><text class="kb-key" onclick="kbKey('7')">7</text><text class="kb-key" onclick="kbKey('8')">8</text><text class="kb-key" onclick="kbKey('9')">9</text><text class="kb-key" onclick="kbKey('0')">0</text>
</div>
<div class="kb-row">
<text class="kb-key kb-phrase" onclick="kbPhrase('帮我看看任务')">帮我看看任务</text>
<text class="kb-key kb-phrase" onclick="kbPhrase('总结一下')">总结一下</text>
<text class="kb-key kb-phrase" onclick="kbPhrase('停止')">停止</text>
<text class="kb-key kb-phrase" onclick="kbPhrase('继续')">继续</text>
</div>
<div class="kb-row">
<text class="kb-key kb-fn" onclick="kbSpace">空格</text>
<text class="kb-key kb-fn" onclick="kbBack">退格</text>
<text class="kb-key kb-fn" onclick="kbClose">收起</text>
<text class="kb-key kb-fn kb-send" onclick="kbSend">发送</text>
</div>
</div>
</div>
<!-- 选项选择弹窗 -->
<div class="popup" if="{{showOptionPicker}}">
<div class="popup-box">
<text class="popup-title">{{pickerTitle}}</text>
<div class="picker-option {{pickerIndex === 0 ? 'picker-active' : ''}}" if="{{pickerOptions[0]}}" onclick="pick0"><text>{{pickerOptions[0]}}</text></div>
<div class="picker-option {{pickerIndex === 1 ? 'picker-active' : ''}}" if="{{pickerOptions[1]}}" onclick="pick1"><text>{{pickerOptions[1]}}</text></div>
<div class="picker-option {{pickerIndex === 2 ? 'picker-active' : ''}}" if="{{pickerOptions[2]}}" onclick="pick2"><text>{{pickerOptions[2]}}</text></div>
<div class="picker-option {{pickerIndex === 3 ? 'picker-active' : ''}}" if="{{pickerOptions[3]}}" onclick="pick3"><text>{{pickerOptions[3]}}</text></div>
<div class="popup-actions">
<input class="popup-btn" type="button" value="确定" onclick="applyPicker" />
</div>
</div>
</div>
<!-- 字体大小弹窗 -->
<div class="popup" if="{{showFontPicker}}">
<div class="popup-box">
<text class="popup-title">字体大小</text>
<slider class="font-slider" min="14" max="22" step="2" value="{{fontValue}}" onchange="onFontChange" />
<text class="popup-value">{{fontLabel}}</text>
<div class="popup-actions">
<input class="popup-btn" type="button" value="确定" onclick="confirmFont" />
</div>
</div>
</div>
</div>
</template>
<script>
import bridge from '../../common/bridge.js'
// 懒加载 @system.audio,避免模拟器/无扬声器时崩
let audioModule = null
let audioTried = false
function loadAudio() {
if (audioTried) return audioModule
audioTried = true
try {
audioModule = require('@system.audio')
} catch (e) {
audioModule = null
}
return audioModule
}
function playDuckSound() {
const a = loadAudio()
if (!a) return
try {
a.src = '/common/Ya1.mp3'
a.play()
} catch (e) {}
}
export default {
private: {
currentIndex: 2,
messages: [
{ role:'user', text:'帮我看看今天有什么任务', preview:'', think:null, kind:'normal', showThink:false, collapsed:false },
{ role:'assistant', text:'好的,今天三件事:1) 回复 DSHWatch 进度;2) 调整手环 UI;3) 准备部署。', preview:'好的,今天三件事:1) 回复 DSHWatch 进度;2) 调整手环…', think:'正在从工作区任务列表整理,并对比上次状态…', kind:'normal', showThink:true, collapsed:true },
{ role:'user', text:'第一件事具体说说', preview:'', think:null, kind:'normal', showThink:false, collapsed:false },
{ role:'assistant', text:'已把进度整理成:1)修复白屏(已提交) 2)布局放大(已提交) 3)待真机验证', preview:'已把进度整理成:1)修复白屏…', think:'汇总最近 5 次提交…', kind:'normal', showThink:true, collapsed:true },
{ role:'assistant', text:'需要一个权限:允许运行网络请求到 https://api.deepseek.com/user/balance', preview:'', think:null, kind:'approval', showThink:false, collapsed:false },
{ role:'user', text:'允许', preview:'', think:null, kind:'normal', showThink:false, collapsed:false },
{ role:'assistant', text:'已查询到余额 ¥6.20,今日已用 ¥12.08。', preview:'', think:'正在调用 DeepSeek user/balance…', kind:'normal', showThink:true, collapsed:false },
{ role:'user', text:'帮我生成一张手环 UI 草图', preview:'', think:null, kind:'normal', showThink:false, collapsed:false },
{ role:'assistant', text:'生成了一张「手表主界面」草图,点击可放大。', preview:'生成了一张「手表主界面」草图…', think:'正在渲染图像…', kind:'image', showThink:true, collapsed:false },
{ role:'assistant', text:'另外建议把左边做成对话流、右边放设置,中间吉祥物可交互——这样最贴合你说的“把 Harness 串流过来”。', preview:'另外建议把左边做成对话流、右边放设置…', think:'正在思考布局建议…', kind:'normal', showThink:true, collapsed:true },
{ role:'system', text:'会话已切换工作区:reactor', preview:'', think:null, kind:'normal', showThink:false, collapsed:false },
{ role:'assistant', text:'正在调用工具 bash 执行 npm run build…', preview:'正在调用工具 bash…', think:null, kind:'tool', tag:'🔧 工具调用', showThink:false, collapsed:true },
{ role:'assistant', text:'构建失败:UxLoader 错误,缺少 system.storage 特性。', preview:'构建失败:UxLoader 错误…', think:null, kind:'error', tag:'❌ 错误', showThink:false, collapsed:false },
{ role:'assistant', text:'方案一:三页结构;方案二:四页+浮动层。', preview:'方案一:三页结构;方案二:四页+浮动层…', think:'正在比较两种布局…', kind:'plan', tag:'🧭 计划', showThink:true, collapsed:true },
{ role:'assistant', text:'const a = 1;\nconsole.log(a);', preview:'const a = 1;…', think:null, kind:'code', tag:'💻 代码', showThink:false, collapsed:true },
{ role:'assistant', text:'模型 | 价格 | 状态\nFlash | 0.1 | 可用', preview:'模型 | 价格 | 状态…', think:null, kind:'table', tag:'📊 表格', showThink:false, collapsed:true },
{ role:'assistant', text:'「把 Harness 串流过来」这个目标,建议先做最小闭环。', preview:'「把 Harness 串流过来」…', think:null, kind:'normal', showThink:false, collapsed:true }
],
workspaces: [
{ name:'DeepSeek_Harness_Watch', conversations:['小米手环10 Pro远程控制','DSHWatch 验收'] },
{ name:'reactor', conversations:['任务-001','任务-002'] },
{ name:'truestudio.online', conversations:['评估-今天'] }
],
workTree: [
{ name:'DeepSeek_Harness_Watch', convs:[ { title:'小米手环10 Pro远程控制', time:'刚刚' }, { title:'DSHWatch 验收', time:'1小时' } ] },
{ name:'reactor', convs:[ { title:'排查Git文件乱码问题', time:'15天' } ] },
{ name:'project', convs:[ { title:'修改API配置为固定密钥', time:'8天' }, { title:'检测DeepSeek API可用性', time:'9天' } ] },
{ name:'truestudio.online', convs:[ { title:'评估一下项目状态', time:'9天' } ] }
],
currentConv: '小米手环10 Pro远程控制',
showSelect: false,
inputText: '',
isPeak: false,
peakLabel: '谷时 · 半价',
balance: '--',
todayTokens: '--',
todayCost: '--',
bubbleVisible: false,
bubbleText: '',
bubbleIndex: 0,
animStep: 0,
animLock: false,
settings: {
serverUrl: 'ws://127.0.0.1:8787',
userId: '',
token: ''
},
connText: '手机/手表未连接',
modelOptions: ['DeepSeek-V4-Flash','DeepSeek-V4-Pro','deepseek-chat','deepseek-reasoner'],
modelIdx: 0,
modelValue: 'DeepSeek-V4-Flash',
providerOptions: ['DeepSeek 官方','Ollama','OpenAI 兼容','DeepSeek 中转'],
providerIdx: 0,
providerValue: 'DeepSeek 官方',
reasonOptions: ['高','中','低','关闭'],
reasonIdx: 0,
reasonValue: '高',
permOptions: ['Workspace Write','Read Only','Ask Once','Full Access'],
permIdx: 0,
permValue: 'Workspace Write',
dialogOptions: ['对话模式','插话模式','深神模式'],
dialogIdx: 0,
dialogModeValue: '对话模式',
connOptions: ['未连接','已连接'],
connIdx: 0,
watchConnValue: '未连接',
serverOptions: ['ws://127.0.0.1:8787','ws://frp-host:18321'],
serverIdx: 0,
serverValue: 'ws://127.0.0.1:8787',
showFontPicker: false,
showKeyboard: false,
showOptionPicker: false,
pickerTitle: '',
pickerOptions: [],
pickerIndex: 0,
pickerTarget: '',
fontValue: 18,
fontLevel: 1,
fontLabel: '中',
bubblePresets: [
'我算得比你快,但没你可爱。',
'上轮消耗 ¥0.08,钱包还撑得住。',
'峰时了,省着点用哦~',
'谷时来了,冲就完事!',
'今天也要一起聪明地偷懒。',
'点我一下,解压一下。',
'上轮余额 ¥12.34,别浪~',
'思考中…其实是发呆。'
]
},
onInit() {
this.refreshPeak()
},
onReady() {
this._unsub = bridge.subscribe({
status: (s) => {
this.connText = s || '未连接'
},
message: (msg) => {
this.handleBridgeMessage(msg)
}
})
bridge.refreshConn()
},
onDestroy() {
if (this._unsub) this._unsub()
},
onSwipeChange(e) {
if (e && typeof e.index === 'number') {
this.currentIndex = e.index
// 离开对话页(index 1)自动收起键盘/选择器
if (this.currentIndex !== 1) {
this.showKeyboard = false
this.showOptionPicker = false
this.showFontPicker = false
}
}
},
toggleSelect() {
this.showSelect = !this.showSelect
},
selectConversation(convIndex, convName) {
this.currentConv = convName || this.currentConv
this.showSelect = false
},
selectConv(name) {
this.currentConv = name
this.currentIndex = 1
this.showSelect = false
},
newSession() {
this.currentConv = '新会话'
this.currentIndex = 1
},
cycleConv() {
const titles = []
for (const ws of this.workTree) {
for (const c of ws.convs) titles.push(c.title)
}
const idx = titles.indexOf(this.currentConv)
this.currentConv = titles[(idx + 1) % titles.length]
},
toggleThink(index) {
const m = this.messages[index]
if (m) m.showThink = !m.showThink
},
toggleCollapse(index) {
const m = this.messages[index]
if (m) m.collapsed = !m.collapsed
},
approve(index) {
const m = this.messages[index]
if (m) { m.kind = 'done'; m.text = '已允许 ✓' }
},
deny(index) {
const m = this.messages[index]
if (m) { m.kind = 'done'; m.text = '已拒绝 ✕' }
},
openInput() {
this.showKeyboard = true
},
kbKey(ch) {
this.inputText = (this.inputText + ch).slice(0, 80)
},
kbPhrase(ph) {
this.inputText = (this.inputText + ph).slice(0, 80)
},
kbSpace() {
this.inputText = (this.inputText + ' ').slice(0, 80)
},
kbBack() {
this.inputText = this.inputText.slice(0, -1)
},
kbClose() {
this.showKeyboard = false
},
kbSend() {
if (this.inputText.trim().length > 0) {
bridge.sendText(this.inputText.trim())
}
this.inputText = ''
this.showKeyboard = false
},
onInputChange(e) {
this.inputText = e && e.value || ''
},
toggleModel() { this.openPicker('model') },
toggleProvider() { this.openPicker('provider') },
toggleReason() { this.openPicker('reason') },
togglePerm() { this.openPicker('perm') },
toggleDialog() { this.openPicker('dialog') },
toggleWatchConn() { this.openPicker('conn') },
toggleServer() { this.openPicker('server') },
openPicker(target) {
const titles = { model:'模型', provider:'提供方', reason:'推理强度', perm:'权限模式', dialog:'对话模式', conn:'手环连接', server:'服务器' }
const values = { model:'modelValue', provider:'providerValue', reason:'reasonValue', perm:'permValue', dialog:'dialogModeValue', conn:'watchConnValue', server:'serverValue' }
this.pickerTitle = titles[target] || '选择'
this.pickerTarget = target
this.pickerOptions = this[target + 'Options']
this.pickerIndex = this[target + 'Idx'] || 0
this.showOptionPicker = true
},
pick0() { this.pickerIndex = 0 },
pick1() { this.pickerIndex = 1 },
pick2() { this.pickerIndex = 2 },
pick3() { this.pickerIndex = 3 },
applyPicker() {
const t = this.pickerTarget
if (!t) return this.showOptionPicker = false
const values = { model:'modelValue', provider:'providerValue', reason:'reasonValue', perm:'permValue', dialog:'dialogModeValue', conn:'watchConnValue', server:'serverValue' }
this[t + 'Idx'] = this.pickerIndex
this[values[t]] = this.pickerOptions[this.pickerIndex]
this.showOptionPicker = false
},
openFontPicker() {
this.showFontPicker = true
this.fontValue = 18
},
onFontChange(e) {
let v = e && typeof e.progress === 'number' ? e.progress : (e && typeof e.value === 'number' ? e.value : 18)
// 兼容 progress 是 0-100 百分比的情况
if (v >= 10 && v <= 24) {
this.fontValue = v
} else {
this.fontValue = Math.round(14 + (v / 100) * (22 - 14))
}
const lv = Math.round((this.fontValue - 14) / 2)
this.fontLevel = Math.max(0, Math.min(4, lv))
this.fontLabel = ['最小', '小', '中', '大', '特大'][this.fontLevel]
},
confirmFont() {
this.showFontPicker = false
const row = this.watchSettings.find((r) => r.label === '字体大小')
if (row) row.value = this.fontLabel
},
refreshPeak() {
// 用北京时间计算峰谷;周六/周日全天谷时
const now = Date.now()
const d = new Date(now + 8 * 3600 * 1000)
const day = d.getUTCDay() // 0=Sun
const hour = d.getUTCHours()
const isWeekend = day === 0 || day === 6
const inPeak = !isWeekend && ((hour >= 9 && hour < 12) || (hour >= 14 && hour < 18))
this.isPeak = inPeak
this.peakLabel = inPeak ? '峰时 · 全价' : '谷时 · 半价'
},
onTap() {
// 冷却:动画/气泡显示期间不重复触发
if (this.animLock) return
this.animLock = true
// Q 弹:底部不动,从上往下收缩,再回弹(scaleY 多帧平滑)
const self = this
this.animStep = 0
setTimeout(() => { self.animStep = 1 }, 0) // scaleY 0.92
setTimeout(() => { self.animStep = 2 }, 40) // scaleY 0.84
setTimeout(() => { self.animStep = 3 }, 85) // scaleY 0.95
setTimeout(() => { self.animStep = 4 }, 135) // scaleY 1.05
setTimeout(() => { self.animStep = 0 }, 200) // scaleY 1.00
// 回弹时播鸭叫
setTimeout(() => { playDuckSound() }, 150)
// 轮播趣味/上轮消耗信息
const idx = this.bubbleIndex % this.bubblePresets.length
this.bubbleIndex += 1
this.bubbleText = this.bubblePresets[idx]
this.bubbleVisible = true
// 气泡显示约 1 秒后自动消失
setTimeout(() => { self.bubbleVisible = false }, 1000)
// 消失后再等 0.5 秒才允许再次点击
setTimeout(() => { self.animLock = false }, 1500)
},
selectSession(index) {
this.currentIndex = 1
},
onServerChange(e) { this.settings.serverUrl = e && e.value || '' },
onUserChange(e) { this.settings.userId = e && e.value || '' },
onTokenChange(e) { this.settings.token = e && e.value || '' },
handleBridgeMessage(msg) {
if (!msg) return
if (msg.type === 'status' && msg.payload) {
const pl = msg.payload
if (typeof pl.balance !== 'undefined') this.balance = pl.balance
if (typeof pl.todayCost !== 'undefined') this.todayCost = pl.todayCost
if (pl.model) this.statusText = '在线 · ' + pl.model
if (typeof pl.running !== 'undefined') this.statusText = pl.running ? '执行中' : '空闲'
this.connText = pl.model ? ('在线 · ' + pl.model) : '已连接'
} else if (msg.type === 'event' && msg.payload) {
// 后续可更新会话/事件列表
}
}
}
</script>
<style>
.page {
width: 100%;
height: 100%;
flex-direction: column;
background-color: #0b1220;
}
.swiper {
flex: 1;
width: 100%;
indicator-size: 0px;
}
.page-item {
width: 100%;
height: 100%;
padding: 12px;
box-sizing: border-box;
}
.left-page,
.right-page {
flex-direction: column;
align-items: flex-start;
padding-top: 22px;
}
.center-page {
flex-direction: column;
justify-content: center;
align-items: center;
}
.bubble-zone {
width: 100%;
height: 54px;
justify-content: center;
align-items: center;
}
.page-title {
color: #e6f0ff;
font-size: 24px;
font-weight: bold;
margin-bottom: 12px;
}
.page-hint {
color: #6b7a99;
font-size: 16px;
margin-top: 20px;
}
.session-list {
width: 100%;
flex-direction: column;
}
.session-row {
width: 100%;
height: 44px;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
padding: 0 10px;
border-radius: 10px;
background-color: #152238;
}
.session-title {
color: #dbe8ff;
font-size: 17px;
}
.session-dot {
color: #33b4ff;
font-size: 16px;
}
/* 吉祥物:放大 + 底部锚定的 scaleY+translateY 补偿 Q 弹 */
.mascot {
width: 456px;
height: 403px;
transition: transform 0.12s cubic-bezier(0.33, 1.12, 0.68, 1);
}
.step0 { transform: translateY(0px) scaleY(1); }
.step1 { transform: translateY(16px) scaleY(0.92); }
.step2 { transform: translateY(36px) scaleY(0.82); }
.step3 { transform: translateY(10px) scaleY(0.95); }
.step4 { transform: translateY(-10px) scaleY(1.05); }
.peak {
width: 100%;
font-size: 32px;
font-weight: bold;
line-height: 40px;
text-align: center;
margin-top: 6px;
}
.peak-green {
color: #3ddc84;
}
.peak-red {
color: #ff5b5b;
}
.stats {
width: 100%;
color: #c4d2ea;
font-size: 22px;
line-height: 28px;
text-align: center;
margin-top: 2px;
}
/* 气泡:头像正上方居中,稍微放大 */
.bubble {
width: 324px;
flex-direction: column;
padding: 18px 20px;
border-radius: 22px;
background-color: rgba(20, 32, 52, 0.86);
border: 1px solid rgba(255, 255, 255, 0.14);
}
.bubble-text {
color: #e6f0ff;
font-size: 17px;
line-height: 24px;
text-align: center;
}
.setting-label {
color: #8fa4c9;
font-size: 14px;
margin-top: 8px;
}
.setting-input {
width: 100%;
height: 38px;
border-radius: 8px;
background-color: #152238;
color: #e6f0ff;
font-size: 15px;
padding: 0 8px;
margin-bottom: 4px;
}
.conn-text {
color: #6b7a99;
font-size: 16px;
margin-top: 8px;
}
.dots {
flex-direction: row;
justify-content: center;
align-items: center;
height: 14px;
margin-top: 4px;
gap: 8px;
}
.dot {
width: 7px;
height: 7px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.28);
}
.dot-active {
width: 22px;
border-radius: 4px;
background-color: #33b4ff;
}
/* ===== 左页:对话 ===== */
.chat-scroll {
flex: 1;
width: 100%;
flex-direction: column;
align-items: flex-start;
}
.chat-img {
width: 100%;
height: 150px;
border-radius: 8px;
margin-top: 4px;
}
.msg-row {
width: 100%;
flex-direction: row;
margin-bottom: 6px;
}
.row-user {
justify-content: flex-end;
}
.row-assistant {
justify-content: flex-start;
}
.chat-msg {
width: auto;
max-width: 84%;
flex-direction: column;
padding: 7px 10px;
border-radius: 12px;
}
.msg-user {
margin-right: 12px;
background-color: rgba(51, 180, 255, 0.20);
border: 1px solid rgba(120, 200, 255, 0.30);
}
.msg-assistant {
margin-left: 12px;
margin-right: 12px;
background-color: rgba(58, 62, 70, 0.55);
border: 1px solid rgba(255, 255, 255, 0.10);
}
.chat-text {
color: #f0f6ff;
font-size: 17px;
line-height: 25px;
}
.think-chip {
background-color: rgba(28, 44, 71, 0.7);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 6px;
padding: 4px;
margin-bottom: 4px;
color: #9db3d8;
font-size: 14px;
}
.think-text {
color: #9db3d8;
font-size: 14px;
line-height: 20px;
margin-bottom: 4px;
}
.expand-chip {
margin-top: 4px;
color: #33b4ff;
font-size: 14px;
}
.approve-box {
flex-direction: row;
margin-top: 6px;
gap: 8px;
}
.approve-btn {
width: 82px;
height: 34px;
border-radius: 17px;
font-size: 14px;
text-align: center;
}
.approve-btn.allow {
background-color: #33b4ff;
color: #08131f;
}
.approve-btn.deny {
background-color: #ff5b5b;
color: #ffffff;
}
.input-bar {
width: 100%;
flex-direction: row;
align-items: center;
margin-top: 6px;
gap: 6px;
}
.input-field {
flex: 1;
min-height: 38px;
height: auto;
border-radius: 8px;
background-color: #152238;
color: #e6f0ff;
font-size: 15px;
padding: 6px 8px;
}
.input-send {
width: 72px;
min-height: 38px;
height: auto;
border-radius: 19px;
background-color: #33b4ff;
color: #08131f;
font-size: 16px;
text-align: center;
}
.select-overlay {
width: 100%;
margin-bottom: 8px;
padding: 8px;
border-radius: 10px;
background-color: #0f1a2c;
}
.select-ws {
margin-bottom: 8px;
}
.select-name {
color: #e6f0ff;
font-size: 15px;
font-weight: bold;
}
.select-conv {
padding: 6px 8px;
margin-top: 4px;
border-radius: 8px;
background-color: #152238;
}
.select-conv-text {
color: #b7c8e8;
font-size: 14px;
}
/* ===== 右页:设置 ===== */
.settings-scroll {
flex: 1;
width: 100%;
height: 100%;
}
.setting-row {
width: 100%;
min-height: 44px;
height: auto;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 6px 10px;
border-radius: 10px;
background-color: #152238;
margin-bottom: 6px;
}
.setting-label {
color: #9db3d8;
font-size: 17px;
}
.setting-value {
color: #e6f0ff;
font-size: 16px;
}
.section-title {
color: #33b4ff;
font-size: 18px;
font-weight: bold;
margin-top: 10px;
margin-bottom: 6px;
}
/* ===== 字体等级(随字号滑块变化) ===== */
.fl0 { font-size: 15px; line-height: 21px; }
.fl1 { font-size: 17px; line-height: 25px; }
.fl2 { font-size: 19px; line-height: 28px; }
.fl3 { font-size: 21px; line-height: 31px; }
.fl4 { font-size: 23px; line-height: 34px; }
/* ===== 字体大小弹窗 ===== */
.popup {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
}
.popup-box {
width: 300px;
min-height: 180px;
flex-direction: column;
align-items: center;
padding: 20px;
border-radius: 18px;
background-color: #0f1a2c;
border: 1px solid rgba(255, 255, 255, 0.14);
}
.popup-title {
color: #e6f0ff;
font-size: 20px;
font-weight: bold;
}
.font-slider {
width: 240px;
margin-top: 14px;
}
.popup-value {
color: #33b4ff;
font-size: 18px;
margin-top: 10px;
}
.popup-actions {
margin-top: 14px;
}
.popup-btn {
width: 120px;
height: 42px;
border-radius: 21px;
background-color: #33b4ff;
color: #08131f;
font-size: 17px;
text-align: center;
}
.picker-option {
width: 260px;
min-height: 40px;
justify-content: center;
align-items: center;
margin-bottom: 6px;
padding: 6px 10px;
border-radius: 10px;
background-color: #152238;
border: 1px solid rgba(255,255,255,0.10);
}
.picker-option text {
color: #dbe8ff;
font-size: 16px;
}
.picker-active {
background-color: rgba(51,180,255,0.22);
border-color: #33b4ff;
}
/* ===== 软键盘 ===== */
.keyboard {
position: absolute;
left: 0;
right: 0;
bottom: 0;
flex-direction: column;
background-color: rgba(11, 18, 32, 0.97);
border-top: 1px solid rgba(255,255,255,0.14);
padding: 10px;
}
.kb-input {
width: 100%;
min-height: 48px;
justify-content: center;
align-items: center;
border-radius: 10px;
background-color: #152238;
margin-bottom: 10px;
padding: 6px;
}
.kb-text {
color: #e6f0ff;
font-size: 17px;
}
.kb-keys {
width: 100%;
flex-direction: column;
}
.kb-row {
width: 100%;
flex-direction: row;
justify-content: center;
margin-bottom: 6px;
}
.kb-key {
width: 34px;
height: 40px;
margin: 0 2px;
justify-content: center;
align-items: center;
border-radius: 8px;
background-color: #1c2c47;
color: #e6f0ff;
font-size: 16px;
text-align: center;
}
.kb-fn {
width: 64px;
}
.kb-phrase {
width: auto;
min-width: 56px;
padding: 0 6px;
}
.kb-send {
background-color: #33b4ff;
color: #08131f;
}
.ws-name {
color: #9db3d8;
font-size: 15px;
font-weight: bold;
margin-top: 10px;
margin-bottom: 4px;
}
.sel-conv {
width: 100%;
min-height: 40px;
flex-direction: row;
justify-content: flex-start;
align-items: center;
padding: 6px 10px;
border-radius: 10px;
background-color: #152238;
margin-bottom: 6px;
}
.sel-conv text {
color: #e6f0ff;
font-size: 16px;
}
</style>