872 lines
22 KiB
Vue
872 lines
22 KiB
Vue
<template>
|
||
<view class="control-page">
|
||
<!-- 顶部导航栏 -->
|
||
<view class="custom-navbar">
|
||
<view class="navbar-content">
|
||
<view class="nav-left" @click="goBack">
|
||
<uni-icons type="back" color="#fff" size="20"></uni-icons>
|
||
</view>
|
||
<view class="nav-center">
|
||
<text class="nav-title">{{ deviceName || 'Six-Channel Mixer' }}</text>
|
||
</view>
|
||
<view class="nav-right">
|
||
<view class="bluetooth-status">
|
||
<uni-icons type="bluetooth-filled" color="#fff" size="20"></uni-icons>
|
||
<text class="status-text">{{ connected ? 'Connected' : 'Disconnected' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="control-container">
|
||
<!-- 无使能通道提示 -->
|
||
<view v-if="enabledChannels.length === 0" class="empty-tip">
|
||
<uni-icons type="info" color="#94a3b8" size="48"></uni-icons>
|
||
<text class="empty-text">No enabled channels</text>
|
||
<text class="empty-hint">Please enable channels in Setting page first</text>
|
||
</view>
|
||
|
||
<!-- 通道状态列表 -->
|
||
<scroll-view v-else class="channel-scroll" scroll-y>
|
||
<view class="channel-list">
|
||
<view
|
||
v-for="channel in enabledChannels"
|
||
:key="channel.id"
|
||
class="channel-card"
|
||
>
|
||
<view class="card-header">
|
||
<text class="channel-title">{{ channel.name }}</text>
|
||
<view :class="['status-badge', getStatusClass(channel.runStatus)]">
|
||
<text>{{ getStatusText(channel.runStatus) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="status-info">
|
||
<!-- 三个模式状态显示 -->
|
||
<view class="mode-status-row">
|
||
<view :class="['mode-item', { active: channel.currentMode === 1, done: channel.currentMode > 1 }]">
|
||
<text class="mode-icon">⚡</text>
|
||
<text class="mode-name">Fast</text>
|
||
<text v-if="channel.currentMode > 1" class="done-mark">✓</text>
|
||
</view>
|
||
<view :class="['mode-item', { active: channel.currentMode === 2, done: channel.currentMode > 2 }]">
|
||
<text class="mode-icon">🔄</text>
|
||
<text class="mode-name">Slow</text>
|
||
<text v-if="channel.currentMode > 2" class="done-mark">✓</text>
|
||
</view>
|
||
<view :class="['mode-item', { active: channel.currentMode === 3, done: channel.runStatus === 3 && channel.currentMode === 3 }]">
|
||
<text class="mode-icon">⏸</text>
|
||
<text class="mode-name">Still</text>
|
||
<text v-if="channel.runStatus === 3" class="done-mark">✓</text>
|
||
</view>
|
||
</view>
|
||
<view class="info-row">
|
||
<text class="info-label">Remaining</text>
|
||
<text class="info-value time">{{ formatTime(channel.remainingTime) }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 进度条 -->
|
||
<view class="progress-bar">
|
||
<view
|
||
class="progress-fill"
|
||
:style="{ width: getProgressPercent(channel) + '%' }"
|
||
></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 控制按钮区域 -->
|
||
<view v-if="enabledChannels.length > 0" class="control-buttons">
|
||
<view class="button-row">
|
||
<view
|
||
:class="['ctrl-btn', 'start', { disabled: !canStart }]"
|
||
@click="handleStart"
|
||
>
|
||
<uni-icons type="circle-filled" color="#fff" size="24"></uni-icons>
|
||
<text>Start</text>
|
||
</view>
|
||
<view
|
||
:class="['ctrl-btn', 'pause', { disabled: !canPause }]"
|
||
@click="handlePause"
|
||
>
|
||
<uni-icons type="minus-filled" color="#fff" size="24"></uni-icons>
|
||
<text>Pause</text>
|
||
</view>
|
||
</view>
|
||
<view class="button-row">
|
||
<view
|
||
:class="['ctrl-btn', 'resume', { disabled: !canResume }]"
|
||
@click="handleResume"
|
||
>
|
||
<uni-icons type="forward" color="#fff" size="24"></uni-icons>
|
||
<text>Resume</text>
|
||
</view>
|
||
<view
|
||
:class="['ctrl-btn', 'stop', { disabled: !canStop }]"
|
||
@click="handleStop"
|
||
>
|
||
<uni-icons type="close" color="#fff" size="24"></uni-icons>
|
||
<text>Exit</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 底部Tab导航 -->
|
||
<view class="bottom-tabs">
|
||
<view class="tab-item" @click="goToSetting">
|
||
<uni-icons type="gear-filled" color="#666" size="24"></uni-icons>
|
||
<text class="tab-text">Setting</text>
|
||
</view>
|
||
<view class="tab-item active">
|
||
<uni-icons type="right" color="#667eea" size="24"></uni-icons>
|
||
<text class="tab-text active">Start</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||
import { onLoad } from '@dcloudio/uni-app'
|
||
|
||
// 本地存储 key
|
||
const STORAGE_KEY = 'mixer_settings'
|
||
|
||
// 蓝牙连接信息
|
||
const connected = ref(false)
|
||
const deviceId = ref('')
|
||
const deviceName = ref('')
|
||
const serviceId = ref('')
|
||
|
||
// 新协议1.2特征值UUID(从设置页面传递)
|
||
const settingCharId = ref('') // 0xFF01 设置参数
|
||
const statusCharId = ref('') // 0xFF02 运行状态
|
||
const controlCharId = ref('') // 0xFF03 控制指令
|
||
|
||
// 定时器
|
||
let statusTimer = null
|
||
|
||
// 通道数据(从设置页面加载)
|
||
const channels = ref([])
|
||
|
||
// 已使能的通道
|
||
const enabledChannels = computed(() => {
|
||
return channels.value.filter(c => c.enabled)
|
||
})
|
||
|
||
// 按钮状态计算
|
||
const canStart = computed(() => {
|
||
return enabledChannels.value.some(c => c.runStatus === 0 || c.runStatus === 3)
|
||
})
|
||
|
||
const canPause = computed(() => {
|
||
return enabledChannels.value.some(c => c.runStatus === 1)
|
||
})
|
||
|
||
const canResume = computed(() => {
|
||
return enabledChannels.value.some(c => c.runStatus === 2)
|
||
})
|
||
|
||
const canStop = computed(() => {
|
||
return enabledChannels.value.some(c => c.runStatus === 1 || c.runStatus === 2)
|
||
})
|
||
|
||
// 页面加载
|
||
onLoad((options) => {
|
||
if (options.deviceId) {
|
||
deviceId.value = options.deviceId
|
||
deviceName.value = decodeURIComponent(options.deviceName || '') || 'Six-Channel Mixer'
|
||
serviceId.value = options.serviceId || ''
|
||
// 接收新协议1.2特征值UUID
|
||
settingCharId.value = options.settingCharId || ''
|
||
statusCharId.value = options.statusCharId || ''
|
||
controlCharId.value = options.controlCharId || ''
|
||
connected.value = true
|
||
}
|
||
|
||
// 加载设置
|
||
loadSettings()
|
||
|
||
// 初始化蓝牙通信
|
||
if (connected.value) {
|
||
initBluetooth()
|
||
}
|
||
})
|
||
|
||
// 页面卸载
|
||
onUnmounted(() => {
|
||
if (statusTimer) {
|
||
clearInterval(statusTimer)
|
||
statusTimer = null
|
||
}
|
||
})
|
||
|
||
// 加载设置
|
||
const loadSettings = () => {
|
||
try {
|
||
const savedData = uni.getStorageSync(STORAGE_KEY)
|
||
if (savedData) {
|
||
const data = JSON.parse(savedData)
|
||
// 合并设置数据和运行状态
|
||
channels.value = (data.channels || []).map(c => ({
|
||
...c,
|
||
runStatus: 0, // 运行状态: 0=等待 1=运行 2=暂停 3=完成
|
||
currentMode: 0, // 当前模式: 1=快速 2=慢速 3=静置
|
||
remainingTime: 0 // 剩余时间(秒)
|
||
}))
|
||
// 如果没有从 URL 接收到特征值,从本地存储读取
|
||
if (!settingCharId.value && data.settingCharId) {
|
||
settingCharId.value = data.settingCharId
|
||
}
|
||
if (!statusCharId.value && data.statusCharId) {
|
||
statusCharId.value = data.statusCharId
|
||
}
|
||
if (!controlCharId.value && data.controlCharId) {
|
||
controlCharId.value = data.controlCharId
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load settings:', e)
|
||
}
|
||
}
|
||
|
||
// 初始化蓝牙
|
||
const initBluetooth = () => {
|
||
// 注册蓝牙数据监听
|
||
uni.onBLECharacteristicValueChange((res) => {
|
||
handleBluetoothData(res)
|
||
})
|
||
|
||
// 发送设置参数
|
||
sendSettings()
|
||
|
||
// 启动状态轮询
|
||
startStatusPolling()
|
||
}
|
||
|
||
// 发送设置参数到设备 (0xFF01)
|
||
const sendSettings = async () => {
|
||
const enabledList = enabledChannels.value
|
||
if (enabledList.length === 0) return
|
||
if (!settingCharId.value) {
|
||
console.error('设置参数特征值UUID未配置')
|
||
return
|
||
}
|
||
|
||
// 顺序发送每个通道的配置,避免蓝牙指令冲突
|
||
for (const channel of enabledList) {
|
||
const index = parseInt(channel.id.replace('CH', ''))
|
||
const buffer = buildSettingBuffer(index, channel)
|
||
await writeBLEDataAsync(settingCharId.value, buffer)
|
||
// 每条指令之间延迟100ms,确保蓝牙设备有时间处理
|
||
await new Promise(resolve => setTimeout(resolve, 100))
|
||
}
|
||
|
||
console.log(`已发送 ${enabledList.length} 条配置指令`)
|
||
}
|
||
|
||
// 构建设置参数buffer
|
||
// 格式: 电机序号(1) + 使能(1) + 快速模式(1) + 快速速度(2) + 快速时间(2) +
|
||
// 慢速模式(1) + 慢速速度(2) + 慢速时间(2) + 静置模式(1) + 静置速度(2) + 静置时间(2)
|
||
const buildSettingBuffer = (motorIndex, channel) => {
|
||
const buffer = new ArrayBuffer(17)
|
||
const view = new Uint8Array(buffer)
|
||
|
||
let offset = 0
|
||
view[offset++] = motorIndex // 电机序号 1-6
|
||
view[offset++] = channel.enabled ? 1 : 0 // 使能
|
||
|
||
// 快速模式
|
||
view[offset++] = 1 // 模式标识
|
||
view[offset++] = channel.fastSpeed & 0xFF // 速度低位
|
||
view[offset++] = (channel.fastSpeed >> 8) & 0xFF // 速度高位
|
||
view[offset++] = channel.fastTime & 0xFF // 时间低位(秒)
|
||
view[offset++] = (channel.fastTime >> 8) & 0xFF // 时间高位
|
||
|
||
// 慢速模式
|
||
view[offset++] = 2 // 模式标识
|
||
view[offset++] = channel.slowSpeed & 0xFF // 速度低位
|
||
view[offset++] = (channel.slowSpeed >> 8) & 0xFF // 速度高位
|
||
const slowTimeSeconds = channel.slowTime * 60 // 转换为秒
|
||
view[offset++] = slowTimeSeconds & 0xFF // 时间低位
|
||
view[offset++] = (slowTimeSeconds >> 8) & 0xFF // 时间高位
|
||
|
||
// 静置模式
|
||
view[offset++] = 3 // 模式标识
|
||
view[offset++] = 0 // 速度低位(固定0)
|
||
view[offset++] = 0 // 速度高位(固定0)
|
||
const stillTimeSeconds = channel.stillTime * 60 // 转换为秒
|
||
view[offset++] = stillTimeSeconds & 0xFF // 时间低位
|
||
view[offset++] = (stillTimeSeconds >> 8) & 0xFF // 时间高位
|
||
|
||
return buffer
|
||
}
|
||
|
||
// 启动状态轮询
|
||
const startStatusPolling = () => {
|
||
if (statusTimer) {
|
||
clearInterval(statusTimer)
|
||
}
|
||
|
||
// 每800ms查询一次状态
|
||
statusTimer = setInterval(() => {
|
||
readStatus()
|
||
}, 800)
|
||
|
||
// 立即查询一次
|
||
readStatus()
|
||
}
|
||
|
||
// 读取运行状态 (0xFF02)
|
||
const readStatus = () => {
|
||
if (!connected.value) return
|
||
if (!statusCharId.value) {
|
||
console.error('运行状态特征值UUID未配置')
|
||
return
|
||
}
|
||
|
||
uni.readBLECharacteristicValue({
|
||
deviceId: deviceId.value,
|
||
serviceId: serviceId.value,
|
||
characteristicId: statusCharId.value,
|
||
success: () => {
|
||
console.log('Read status success')
|
||
},
|
||
fail: (err) => {
|
||
console.error('Read status failed:', err)
|
||
}
|
||
})
|
||
}
|
||
|
||
// 处理蓝牙数据
|
||
const handleBluetoothData = (res) => {
|
||
const dataView = new Uint8Array(res.value)
|
||
const charId = res.characteristicId.toUpperCase()
|
||
|
||
// 判断是状态数据 (0xFF02)
|
||
if (charId.includes('FF02') || charId === statusCharId.value.toUpperCase()) {
|
||
parseStatusData(dataView)
|
||
}
|
||
}
|
||
|
||
// 解析状态数据
|
||
// 格式: [电机序号(1) + 使能(1) + 运行状态(1) + 工作模式(1) + 剩余时间(2)] × 6
|
||
const parseStatusData = (dataView) => {
|
||
const bytesPerMotor = 6
|
||
const motorCount = Math.floor(dataView.length / bytesPerMotor)
|
||
|
||
for (let i = 0; i < motorCount && i < 6; i++) {
|
||
const offset = i * bytesPerMotor
|
||
const motorIndex = dataView[offset]
|
||
const enabled = dataView[offset + 1]
|
||
const runStatus = dataView[offset + 2]
|
||
const workMode = dataView[offset + 3]
|
||
const remainingTime = dataView[offset + 4] | (dataView[offset + 5] << 8)
|
||
|
||
// 更新对应通道数据
|
||
const channel = channels.value.find(c => c.id === `CH${motorIndex}`)
|
||
if (channel) {
|
||
channel.runStatus = runStatus
|
||
channel.currentMode = workMode
|
||
channel.remainingTime = remainingTime
|
||
}
|
||
}
|
||
}
|
||
|
||
// 发送控制指令 (0xFF03)
|
||
// 指令码: 1=启动 2=暂停 3=继续 4=退出
|
||
const sendControl = (command) => {
|
||
const enabledList = enabledChannels.value
|
||
if (enabledList.length === 0) return
|
||
if (!controlCharId.value) {
|
||
console.error('控制指令特征值UUID未配置')
|
||
return
|
||
}
|
||
|
||
// 构建buffer: [电机序号, 指令码] × n
|
||
const buffer = new ArrayBuffer(enabledList.length * 2)
|
||
const view = new Uint8Array(buffer)
|
||
|
||
enabledList.forEach((channel, i) => {
|
||
const motorIndex = parseInt(channel.id.replace('CH', ''))
|
||
view[i * 2] = motorIndex
|
||
view[i * 2 + 1] = command
|
||
})
|
||
|
||
writeBLEData(controlCharId.value, buffer)
|
||
}
|
||
|
||
// 写入蓝牙数据(异步版本)
|
||
const writeBLEDataAsync = (characteristicId, buffer) => {
|
||
return new Promise((resolve, reject) => {
|
||
if (!connected.value) {
|
||
uni.showToast({ title: 'Bluetooth not connected', icon: 'none' })
|
||
reject(new Error('Bluetooth not connected'))
|
||
return
|
||
}
|
||
|
||
uni.writeBLECharacteristicValue({
|
||
deviceId: deviceId.value,
|
||
serviceId: serviceId.value,
|
||
characteristicId: characteristicId,
|
||
value: buffer,
|
||
success: () => {
|
||
console.log('Write success:', characteristicId)
|
||
resolve()
|
||
},
|
||
fail: (err) => {
|
||
console.error('Write failed:', characteristicId, err)
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
// 写入蓝牙数据
|
||
const writeBLEData = (characteristicId, buffer) => {
|
||
if (!connected.value) {
|
||
uni.showToast({ title: 'Bluetooth not connected', icon: 'none' })
|
||
return
|
||
}
|
||
|
||
uni.writeBLECharacteristicValue({
|
||
deviceId: deviceId.value,
|
||
serviceId: serviceId.value,
|
||
characteristicId: characteristicId,
|
||
value: buffer,
|
||
success: () => {
|
||
console.log('Write success:', characteristicId)
|
||
},
|
||
fail: (err) => {
|
||
console.error('Write failed:', characteristicId, err)
|
||
uni.showToast({ title: 'Send failed', icon: 'none' })
|
||
}
|
||
})
|
||
}
|
||
|
||
// 控制按钮事件
|
||
const handleStart = () => {
|
||
if (!canStart.value) return
|
||
sendControl(1)
|
||
uni.showToast({ title: 'Starting...', icon: 'none' })
|
||
}
|
||
|
||
const handlePause = () => {
|
||
if (!canPause.value) return
|
||
sendControl(2)
|
||
uni.showToast({ title: 'Paused', icon: 'none' })
|
||
}
|
||
|
||
const handleResume = () => {
|
||
if (!canResume.value) return
|
||
sendControl(3)
|
||
uni.showToast({ title: 'Resuming...', icon: 'none' })
|
||
}
|
||
|
||
const handleStop = () => {
|
||
if (!canStop.value) return
|
||
|
||
uni.showModal({
|
||
title: 'Confirm Exit',
|
||
content: 'Are you sure to exit the current workflow?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
sendControl(4)
|
||
uni.showToast({ title: 'Exited', icon: 'none' })
|
||
}
|
||
}
|
||
})
|
||
}
|
||
|
||
// 获取状态文本
|
||
const getStatusText = (status) => {
|
||
const map = { 0: 'Waiting', 1: 'Running', 2: 'Paused', 3: 'Completed' }
|
||
return map[status] || 'Unknown'
|
||
}
|
||
|
||
// 获取状态样式类
|
||
const getStatusClass = (status) => {
|
||
const map = { 0: 'waiting', 1: 'running', 2: 'paused', 3: 'completed' }
|
||
return map[status] || 'waiting'
|
||
}
|
||
|
||
// 获取模式文本
|
||
const getModeText = (mode) => {
|
||
const map = { 0: '-', 1: 'Fast Mode', 2: 'Slow Mode', 3: 'Still Mode' }
|
||
return map[mode] || '-'
|
||
}
|
||
|
||
// 格式化时间
|
||
const formatTime = (seconds) => {
|
||
if (!seconds || seconds <= 0) return '--:--'
|
||
const min = Math.floor(seconds / 60)
|
||
const sec = seconds % 60
|
||
return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`
|
||
}
|
||
|
||
// 计算进度百分比
|
||
const getProgressPercent = (channel) => {
|
||
if (channel.runStatus === 0 || channel.runStatus === 3) return 0
|
||
|
||
// 计算总时间
|
||
let totalTime = 0
|
||
if (channel.currentMode === 1) {
|
||
totalTime = channel.fastTime
|
||
} else if (channel.currentMode === 2) {
|
||
totalTime = channel.slowTime * 60
|
||
} else if (channel.currentMode === 3) {
|
||
totalTime = channel.stillTime * 60
|
||
}
|
||
|
||
if (totalTime <= 0) return 0
|
||
const elapsed = totalTime - channel.remainingTime
|
||
return Math.min(100, Math.max(0, (elapsed / totalTime) * 100))
|
||
}
|
||
|
||
// 返回/跳转到设置页面
|
||
const goBack = () => {
|
||
const pages = getCurrentPages()
|
||
if (pages.length > 1) {
|
||
uni.navigateBack()
|
||
} else {
|
||
// 页面栈为空(预览模式),直接跳转
|
||
uni.redirectTo({ url: '/pages/mixer/setting' })
|
||
}
|
||
}
|
||
|
||
// 跳转到设置页面
|
||
const goToSetting = () => {
|
||
goBack()
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.control-page {
|
||
width: 100%;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
}
|
||
|
||
// 自定义导航栏
|
||
.custom-navbar {
|
||
width: 100%;
|
||
background: rgba(102, 126, 234, 0.9);
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 999;
|
||
padding-top: max(env(safe-area-inset-top), 20px);
|
||
|
||
.navbar-content {
|
||
height: 44px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 16rpx;
|
||
}
|
||
|
||
.nav-center {
|
||
position: absolute;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
|
||
.nav-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.nav-left, .nav-right {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.bluetooth-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
|
||
.status-text {
|
||
font-size: 24rpx;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
.control-container {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding-top: max(calc(44px + env(safe-area-inset-top)), 64px);
|
||
padding-bottom: 120rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
// 空状态提示
|
||
.empty-tip {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 16rpx;
|
||
|
||
.empty-text {
|
||
font-size: 32rpx;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.empty-hint {
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.7);
|
||
}
|
||
}
|
||
|
||
.channel-scroll {
|
||
flex: 1;
|
||
padding: 16rpx 20rpx;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.channel-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
}
|
||
|
||
.channel-card {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 16rpx;
|
||
padding: 20rpx;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
|
||
.channel-title {
|
||
font-size: 32rpx;
|
||
font-weight: 700;
|
||
color: #1e293b;
|
||
}
|
||
}
|
||
|
||
.status-badge {
|
||
padding: 6rpx 16rpx;
|
||
border-radius: 20rpx;
|
||
font-size: 22rpx;
|
||
font-weight: 600;
|
||
|
||
&.waiting {
|
||
background: #f1f5f9;
|
||
color: #64748b;
|
||
}
|
||
|
||
&.running {
|
||
background: #dcfce7;
|
||
color: #16a34a;
|
||
}
|
||
|
||
&.paused {
|
||
background: #fef3c7;
|
||
color: #d97706;
|
||
}
|
||
|
||
&.completed {
|
||
background: #dbeafe;
|
||
color: #2563eb;
|
||
}
|
||
}
|
||
|
||
.status-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.mode-status-row {
|
||
display: flex;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.mode-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 8rpx;
|
||
border-radius: 8rpx;
|
||
background: #f1f5f9;
|
||
position: relative;
|
||
|
||
.mode-icon {
|
||
font-size: 24rpx;
|
||
}
|
||
|
||
.mode-name {
|
||
font-size: 20rpx;
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.done-mark {
|
||
position: absolute;
|
||
top: 2rpx;
|
||
right: 4rpx;
|
||
font-size: 18rpx;
|
||
color: #22c55e;
|
||
font-weight: bold;
|
||
}
|
||
|
||
&.active {
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
|
||
.mode-name {
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
&.done {
|
||
background: #dcfce7;
|
||
|
||
.mode-name {
|
||
color: #16a34a;
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-row {
|
||
flex: 1;
|
||
|
||
.info-label {
|
||
font-size: 22rpx;
|
||
color: #94a3b8;
|
||
display: block;
|
||
margin-bottom: 4rpx;
|
||
}
|
||
|
||
.info-value {
|
||
font-size: 28rpx;
|
||
color: #334155;
|
||
font-weight: 600;
|
||
|
||
&.time {
|
||
font-family: monospace;
|
||
color: #667eea;
|
||
}
|
||
}
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 8rpx;
|
||
background: #e2e8f0;
|
||
border-radius: 4rpx;
|
||
overflow: hidden;
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #667eea, #764ba2);
|
||
border-radius: 4rpx;
|
||
transition: width 0.3s ease;
|
||
}
|
||
}
|
||
|
||
// 控制按钮区域
|
||
.control-buttons {
|
||
padding: 20rpx;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.button-row {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
margin-bottom: 16rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.ctrl-btn {
|
||
flex: 1;
|
||
height: 88rpx;
|
||
border-radius: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: #fff;
|
||
|
||
&.start {
|
||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||
}
|
||
|
||
&.pause {
|
||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
||
}
|
||
|
||
&.resume {
|
||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||
}
|
||
|
||
&.stop {
|
||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||
}
|
||
|
||
&.disabled {
|
||
opacity: 0.5;
|
||
pointer-events: none;
|
||
}
|
||
|
||
&:active {
|
||
transform: scale(0.98);
|
||
}
|
||
}
|
||
|
||
// 底部Tab导航
|
||
.bottom-tabs {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 100rpx;
|
||
background: #fff;
|
||
display: flex;
|
||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||
padding-bottom: env(safe-area-inset-bottom);
|
||
}
|
||
|
||
.tab-item {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4rpx;
|
||
|
||
.tab-text {
|
||
font-size: 22rpx;
|
||
color: #666;
|
||
|
||
&.active {
|
||
color: #667eea;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
</style>
|