1100 lines
33 KiB
XML
1100 lines
33 KiB
XML
<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>
|