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

794 lines
21 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">
<!-- 左页:对话(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="chat-msg {{$item.role === 'user' ? 'msg-user' : 'msg-assistant'}}" for="{{messages}}">
<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>
</scroll>
<!-- 底部输入栏 -->
<div class="input-bar">
<input class="input-field" type="text" value="{{inputText}}" placeholder="说点什么…" onchange="onInputChange" />
<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>
<!-- 字体大小弹窗 -->
<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: 1,
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 }
],
workspaces: [
{ name:'DeepSeek_Harness_Watch', conversations:['小米手环10 Pro远程控制','DSHWatch 验收'] },
{ name:'reactor', conversations:['任务-001','任务-002'] },
{ name:'truestudio.online', conversations:['评估-今天'] }
],
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: '手机/手表未连接',
modelValue: 'DeepSeek-V4-Flash',
providerValue: 'DeepSeek 官方',
reasonValue: '高',
permValue: 'Workspace Write',
dialogModeValue: '深神模式',
watchConnValue: '未连接(占位)',
serverValue: 'ws://127.0.0.1:8787',
showFontPicker: false,
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
}
},
toggleSelect() {
this.showSelect = !this.showSelect
},
selectConversation(convIndex, convName) {
this.currentConv = convName || this.currentConv
this.showSelect = false
},
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.inputText = ''
},
onInputChange(e) {
this.inputText = e && e.value || ''
},
toggleModel() { this.modelValue += ' ·' },
toggleProvider() { this.providerValue += ' ·' },
toggleReason() { this.reasonValue += ' ·' },
togglePerm() { this.permValue += ' ·' },
toggleDialog() { this.dialogModeValue += ' ·' },
toggleWatchConn() { this.watchConnValue += ' ·' },
toggleServer() { this.serverValue += ' ·' },
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;
}
.chat-img {
width: 100%;
height: 150px;
border-radius: 8px;
margin-top: 4px;
}
.chat-msg {
width: 100%;
flex-direction: column;
margin-bottom: 6px;
padding: 7px 9px;
border-radius: 10px;
}
.msg-user {
background-color: rgba(51, 180, 255, 0.18);
border: 1px solid rgba(120, 200, 255, 0.28);
}
.msg-assistant {
background-color: rgba(22, 34, 56, 0.58);
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;
}
</style>