feat: single-mascot main page — no buttons, tap bubble + bounce + peak/off-peak pricing, balance/usage

This commit is contained in:
2026-09-01 20:11:50 +08:00
parent aacabb6c9a
commit 96e13a85b5
+82 -246
View File
@@ -1,138 +1,74 @@
<template> <template>
<div class="page"> <div class="page">
<swiper class="swiper" index="{{currentIndex}}" indicator="false" loop="false" <!-- 吉祥物:唯一主体,点击互动 -->
previousmargin="14px" nextmargin="14px" onchange="onSwipeChange"> <image class="mascot {{tapAnim ? 'mascot-bounce' : ''}}" src="/common/dsniang.png" onclick="onTap" />
<!-- 左页:会话 -->
<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"> <text class="peak {{isPeak ? 'peak-red' : 'peak-green'}}">{{peakLabel}}</text>
<image class="mascot" src="/common/dsniang.png" onclick="openDetail" />
<text class="status-text">{{statusText}}</text>
<text class="session-hint">{{activeTitle}}</text>
<div class="center-actions">
<input class="btn" type="button" value="输入" onclick="openInput" />
<input class="btn btn-ghost" type="button" value="详情" onclick="openDetail" />
</div>
</div>
<!-- 右页:设置 --> <!-- Token 使用统计 / 余额 -->
<div class="page-item right-page"> <text class="stats">余额 {{balance}}</text>
<text class="page-title">设置</text> <text class="stats">今日 {{todayTokens}} tokens · 预估 ¥{{todayCost}}</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>
<input class="btn" type="button" value="连接状态" onclick="refreshConn" />
<input class="btn btn-ghost" type="button" value="更多设置" onclick="openSettings" />
</div>
</swiper>
<div class="dots"> <!-- 点击吉祥物弹出的“对话小框”(类 DSH 挂件气泡) -->
<div class="dot {{currentIndex == 0 ? 'dot-active' : ''}}"></div> <div class="bubble" if="{{bubbleVisible}}">
<div class="dot {{currentIndex == 1 ? 'dot-active' : ''}}"></div> <text class="bubble-text">{{bubbleText}}</text>
<div class="dot {{currentIndex == 2 ? 'dot-active' : ''}}"></div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import router from '@system.router'
import bridge from '../../common/bridge.js'
export default { export default {
private: { private: {
currentIndex: 1, isPeak: false,
sessions: [], peakLabel: '谷时 · 半价',
statusText: '加载中…', balance: '--',
activeTitle: 'DSHWatch 初始化中…', todayTokens: '--',
settings: { todayCost: '--',
serverUrl: 'ws://127.0.0.1:8787', bubbleVisible: false,
userId: '', bubbleText: '',
token: '' tapAnim: false,
}, lines: [
connText: '正在连接…' '我算得比你快,但没你可爱。',
'余额在跳,我也在跳,别慌。',
'峰时了,省着点用哦~',
'谷时来了,冲就完事!',
'今天也要一起聪明地偷懒。',
'点我一下,解压一下。'
]
}, },
onInit() { onInit() {
// 空壳:仅预留 this.refreshPeak()
}, },
onReady() { onReady() {
this._unsub = bridge.subscribe({ // 后续接服务器时在这里拉余额/用量
status: (s) => {
this.statusText = s
this.connText = s
},
message: (msg) => {
this.handleBridgeMessage(msg)
}
})
bridge.refreshConn()
}, },
onDestroy() { refreshPeak() {
if (this._unsub) this._unsub() // 用北京时间计算峰谷;周六/周日全天谷时
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 ? '峰时 · 全价' : '谷时 · 半价'
}, },
onSwipeChange(e) { onTap() {
if (e && typeof e.index === 'number') { // Q 弹互动
this.currentIndex = e.index this.tapAnim = true
} const self = this
}, setTimeout(() => { self.tapAnim = false }, 400)
goTo(index) { // 对话小框:显示一句幽默文案 + 峰谷信息
// index 属性绑定已驱动 swiper 切换;此处主要同步 data const idx = Math.floor(Math.random() * this.lines.length)
this.currentIndex = index const mode = this.isPeak ? '当前是峰时,钱包瑟瑟发抖' : '当前是谷时,薅羊毛好时机'
}, this.bubbleText = this.lines[idx] + ' | ' + mode
this.bubbleVisible = !this.bubbleVisible
selectSession(index) {
const item = this.sessions[index]
if (item) {
this.activeTitle = '会话 ' + item.title
this.goTo(1)
}
},
openDetail() {
router.push({ uri: '/pages/detail' })
},
openInput() {
router.push({ uri: '/pages/input' })
},
openSettings() {
router.push({ uri: '/pages/settings' })
},
onServerChange(e) { this.settings.serverUrl = e && e.value || '' },
onUserChange(e) { this.settings.userId = e && e.value || '' },
onTokenChange(e) { this.settings.token = e && e.value || '' },
refreshConn() {
bridge.refreshConn()
},
handleBridgeMessage(msg) {
// 空壳:按 PROTOCOL.md 处理手机下发的 status/event 等
if (msg && msg.type === 'status' && msg.payload) {
this.statusText = msg.payload.statusText || this.statusText
this.activeTitle = msg.payload.activeTitle || this.activeTitle
if (msg.payload.sessions) this.sessions = msg.payload.sessions
}
} }
} }
</script> </script>
@@ -142,154 +78,54 @@ export default {
width: 100%; width: 100%;
height: 100%; height: 100%;
flex-direction: column; flex-direction: column;
justify-content: center;
align-items: center;
background-color: #0b1220; 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;
}
.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;
}
.mascot { .mascot {
width: 180px; width: 220px;
height: 180px; height: 220px;
} }
.status-text { .mascot-bounce {
color: #e6f0ff; transform: scale(0.92);
}
.peak {
font-size: 18px; font-size: 18px;
margin-top: 14px; font-weight: bold;
margin-top: 10px;
} }
.session-hint { .peak-green {
color: #7d8fb0; color: #3ddc84;
}
.peak-red {
color: #ff5b5b;
}
.stats {
color: #9fb0d0;
font-size: 14px; font-size: 14px;
margin-top: 4px; margin-top: 6px;
} }
.center-actions { .bubble {
flex-direction: row; position: absolute;
margin-top: 18px; top: 90px;
gap: 12px; left: 50%;
margin-left: -110px;
width: 220px;
padding: 10px 12px;
border-radius: 14px;
background-color: #ffffff;
} }
.btn { .bubble-text {
width: 100px; color: #0b1220;
height: 40px; font-size: 14px;
border-radius: 20px;
background-color: #33b4ff;
color: #08131f;
font-size: 16px;
text-align: center; text-align: center;
} }
.btn-ghost {
background-color: #1c2c47;
color: #dbe8ff;
}
.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> </style>