379 lines
8.4 KiB
XML
379 lines
8.4 KiB
XML
<template>
|
|
<div class="page">
|
|
<swiper class="swiper" index="{{currentIndex}}" indicator="false" loop="false"
|
|
previousmargin="14px" nextmargin="14px" onchange="onSwipeChange">
|
|
|
|
<!-- 左页:会话 -->
|
|
<div class="page-item left-page">
|
|
<text class="page-title">会话</text>
|
|
<div class="session-list">
|
|
<div class="session-row" for="{{sessions}}" onclick="selectSession($index)">
|
|
<text class="session-title">{{$item.title}}</text>
|
|
<text class="session-dot" if="{{$item.running}}">●</text>
|
|
</div>
|
|
</div>
|
|
<text class="page-hint" if="{{!sessions.length}}">暂无会话</text>
|
|
</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>
|
|
<text class="setting-label">服务器</text>
|
|
<input class="setting-input" type="text" value="{{settings.serverUrl}}" onchange="onServerChange" />
|
|
<text class="setting-label">用户ID</text>
|
|
<input class="setting-input" type="text" value="{{settings.userId}}" onchange="onUserChange" />
|
|
<text class="setting-label">Token</text>
|
|
<input class="setting-input" type="text" value="{{settings.token}}" onchange="onTokenChange" />
|
|
<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>
|
|
</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,
|
|
sessions: [
|
|
{ title: '原型演示会话', running: false },
|
|
{ title: 'UI 对齐中', running: true },
|
|
{ title: '待接入 Harness', running: false }
|
|
],
|
|
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: '手机/手表未连接',
|
|
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
|
|
}
|
|
},
|
|
|
|
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)
|
|
// 冷却结束
|
|
setTimeout(() => { self.animLock = false }, 360)
|
|
|
|
// 轮播趣味/上轮消耗信息
|
|
const idx = this.bubbleIndex % this.bubblePresets.length
|
|
this.bubbleIndex += 1
|
|
this.bubbleText = this.bubblePresets[idx]
|
|
this.bubbleVisible = true
|
|
},
|
|
|
|
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) {
|
|
// 空壳:后续按 PROTOCOL.md 处理 status/event
|
|
}
|
|
}
|
|
</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;
|
|
}
|
|
|
|
.center-page {
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.bubble-zone {
|
|
width: 100%;
|
|
height: 110px;
|
|
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;
|
|
}
|
|
|
|
/* 吉祥物:比之前大一点 + 敲击 Q 弹 */
|
|
.mascot {
|
|
width: 320px;
|
|
height: 320px;
|
|
transform-origin: center bottom;
|
|
transition: transform 0.14s cubic-bezier(0.33, 1.12, 0.68, 1);
|
|
}
|
|
|
|
.step0 {
|
|
transform: scaleY(1);
|
|
}
|
|
|
|
.step1 {
|
|
transform: scaleY(0.92);
|
|
}
|
|
|
|
.step2 {
|
|
transform: scaleY(0.82);
|
|
}
|
|
|
|
.step3 {
|
|
transform: scaleY(0.95);
|
|
}
|
|
|
|
.step4 {
|
|
transform: scaleY(1.05);
|
|
}
|
|
|
|
.peak {
|
|
font-size: 22px;
|
|
font-weight: bold;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.peak-green {
|
|
color: #3ddc84;
|
|
}
|
|
|
|
.peak-red {
|
|
color: #ff5b5b;
|
|
}
|
|
|
|
.stats {
|
|
color: #c4d2ea;
|
|
font-size: 16px;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
/* 气泡:头像正上方居中,稍微放大 */
|
|
.bubble {
|
|
width: 256px;
|
|
flex-direction: column;
|
|
padding: 12px 14px;
|
|
border-radius: 16px;
|
|
background-color: #ffffff;
|
|
}
|
|
|
|
.bubble-text {
|
|
color: #1c2c47;
|
|
font-size: 15px;
|
|
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: 14px;
|
|
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;
|
|
}
|
|
</style>
|