Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb5650cf6 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -10,8 +10,3 @@ docs/_book
|
||||
test/
|
||||
|
||||
node_modules
|
||||
/.idea/.gitignore
|
||||
/.idea/intc-mixer-app.iml
|
||||
/.idea/modules.xml
|
||||
/.idea/vcs.xml
|
||||
/dist/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 136 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 110 KiB |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name" : "MixerControl",
|
||||
"name" : "搅拌器控制",
|
||||
"appid" : "__UNI__0373DF9",
|
||||
"description" : "MixerControl",
|
||||
"versionName" : "1.2.0",
|
||||
"description" : "搅拌器控制软件",
|
||||
"versionName" : "1.1.0",
|
||||
"versionCode" : "100",
|
||||
"transformPx" : false,
|
||||
/* 5+App特有相关 */
|
||||
@@ -55,10 +55,10 @@
|
||||
"sdkConfigs" : {},
|
||||
"icons" : {
|
||||
"android" : {
|
||||
"hdpi" : "mixerControlE.png",
|
||||
"xhdpi" : "mixerControlE.png",
|
||||
"xxhdpi" : "mixerControlE.png",
|
||||
"xxxhdpi" : "mixerControlE.png"
|
||||
"hdpi" : "mixer.png",
|
||||
"xhdpi" : "mixer.png",
|
||||
"xxhdpi" : "mixer.png",
|
||||
"xxxhdpi" : "mixer.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,5 @@
|
||||
"uniStatistics" : {
|
||||
"enable" : false
|
||||
},
|
||||
"vueVersion" : "3",
|
||||
"locale" : "en"
|
||||
"vueVersion" : "3"
|
||||
}
|
||||
|
||||
@@ -33,20 +33,6 @@
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mixer/setting",
|
||||
"style": {
|
||||
"navigationBarTitleText": "搅拌器设置",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/mixer/control",
|
||||
"style": {
|
||||
"navigationBarTitleText": "搅拌器控制",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/register",
|
||||
"style": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,17 +12,17 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Title -->
|
||||
<!-- 标题 -->
|
||||
<view class="title-section">
|
||||
<text class="main-title">Mixer Control Connection</text>
|
||||
<text class="sub-title">Quickly search and connect to nearby mixer Bluetooth devices</text>
|
||||
<text class="main-title">搅拌器控制连接</text>
|
||||
<text class="sub-title">快速搜索并连接附近搅拌器蓝牙设备</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<view class="btn-section">
|
||||
<button @click="handleLogin" class="login-btn">
|
||||
<uni-icons type="right" size="24" color="#667eea" style="margin-right: 12rpx;"></uni-icons>
|
||||
<text class="btn-text">Enter</text>
|
||||
<text class="btn-text">进入</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -1,871 +0,0 @@
|
||||
<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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,568 +0,0 @@
|
||||
<template>
|
||||
<view class="setting-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="setting-container">
|
||||
<!-- 通道卡片网格 -->
|
||||
<scroll-view class="channel-scroll" scroll-y>
|
||||
<view class="channel-grid">
|
||||
<view
|
||||
v-for="channel in channels"
|
||||
:key="channel.id"
|
||||
:class="['channel-card', { 'disabled': !channel.enabled }]"
|
||||
>
|
||||
<!-- 通道标题和使能开关 -->
|
||||
<view class="card-header">
|
||||
<text class="channel-title">{{ channel.name }}</text>
|
||||
<view
|
||||
:class="['custom-switch', { checked: channel.enabled }]"
|
||||
@click="toggleEnabled(channel.id)"
|
||||
>
|
||||
<view class="switch-slider"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Fast Mode -->
|
||||
<view class="mode-section">
|
||||
<view class="mode-title">
|
||||
<text class="mode-icon">⚡</text>
|
||||
<text>Fast Mode</text>
|
||||
</view>
|
||||
<view class="mode-row">
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">Speed</text>
|
||||
<picker
|
||||
:value="getFastSpeedIndex(channel.id)"
|
||||
:range="fastSpeedOptions"
|
||||
@change="e => updateFastSpeed(channel.id, e.detail.value)"
|
||||
:disabled="!channel.enabled"
|
||||
>
|
||||
<view class="picker-value">{{ channel.fastSpeed }} RPM</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">Time</text>
|
||||
<picker
|
||||
:value="getFastTimeIndex(channel.id)"
|
||||
:range="fastTimeOptions"
|
||||
@change="e => updateFastTime(channel.id, e.detail.value)"
|
||||
:disabled="!channel.enabled"
|
||||
>
|
||||
<view class="picker-value">{{ channel.fastTime }} s</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Slow Mode -->
|
||||
<view class="mode-section">
|
||||
<view class="mode-title">
|
||||
<text class="mode-icon">🔄</text>
|
||||
<text>Slow Mode</text>
|
||||
</view>
|
||||
<view class="mode-row">
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">Speed</text>
|
||||
<picker
|
||||
:value="getSlowSpeedIndex(channel.id)"
|
||||
:range="slowSpeedOptions"
|
||||
@change="e => updateSlowSpeed(channel.id, e.detail.value)"
|
||||
:disabled="!channel.enabled"
|
||||
>
|
||||
<view class="picker-value">{{ channel.slowSpeed }} RPM</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">Time</text>
|
||||
<picker
|
||||
:value="getSlowTimeIndex(channel.id)"
|
||||
:range="slowTimeOptions"
|
||||
@change="e => updateSlowTime(channel.id, e.detail.value)"
|
||||
:disabled="!channel.enabled"
|
||||
>
|
||||
<view class="picker-value">{{ channel.slowTime }} min</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Still Mode -->
|
||||
<view class="mode-section">
|
||||
<view class="mode-title">
|
||||
<text class="mode-icon">⏸</text>
|
||||
<text>Still Mode</text>
|
||||
</view>
|
||||
<view class="mode-row">
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">Speed</text>
|
||||
<view class="picker-value fixed">0 RPM</view>
|
||||
</view>
|
||||
<view class="setting-item">
|
||||
<text class="setting-label">Time</text>
|
||||
<picker
|
||||
:value="getStillTimeIndex(channel.id)"
|
||||
:range="stillTimeOptions"
|
||||
@change="e => updateStillTime(channel.id, e.detail.value)"
|
||||
:disabled="!channel.enabled"
|
||||
>
|
||||
<view class="picker-value">{{ channel.stillTime }} min</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 底部Tab导航 -->
|
||||
<view class="bottom-tabs">
|
||||
<view class="tab-item active">
|
||||
<uni-icons type="gear-filled" color="#667eea" size="24"></uni-icons>
|
||||
<text class="tab-text active">Setting</text>
|
||||
</view>
|
||||
<view class="tab-item" @click="goToControl">
|
||||
<uni-icons type="right" color="#666" size="24"></uni-icons>
|
||||
<text class="tab-text">Start</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } 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 控制指令
|
||||
|
||||
// 下拉选项
|
||||
const fastSpeedValues = [50, 100, 200, 300, 400]
|
||||
const fastSpeedOptions = fastSpeedValues.map(v => `${v} RPM`)
|
||||
const fastTimeValues = [0, 30, 60, 90, 120]
|
||||
const fastTimeOptions = fastTimeValues.map(v => `${v} s`)
|
||||
|
||||
const slowSpeedValues = [10, 20, 30, 40, 50]
|
||||
const slowSpeedOptions = slowSpeedValues.map(v => `${v} RPM`)
|
||||
const slowTimeValues = [0, 10, 20, 30]
|
||||
const slowTimeOptions = slowTimeValues.map(v => `${v} min`)
|
||||
|
||||
const stillTimeValues = [0, 10, 20, 30]
|
||||
const stillTimeOptions = stillTimeValues.map(v => `${v} min`)
|
||||
|
||||
// 通道数据
|
||||
const channels = ref([
|
||||
{ id: 'CH1', name: 'CH1', enabled: false, fastSpeed: 100, fastTime: 60, slowSpeed: 20, slowTime: 10, stillTime: 10 },
|
||||
{ id: 'CH2', name: 'CH2', enabled: false, fastSpeed: 100, fastTime: 60, slowSpeed: 20, slowTime: 10, stillTime: 10 },
|
||||
{ id: 'CH3', name: 'CH3', enabled: false, fastSpeed: 100, fastTime: 60, slowSpeed: 20, slowTime: 10, stillTime: 10 },
|
||||
{ id: 'CH4', name: 'CH4', enabled: false, fastSpeed: 100, fastTime: 60, slowSpeed: 20, slowTime: 10, stillTime: 10 },
|
||||
{ id: 'CH5', name: 'CH5', enabled: false, fastSpeed: 100, fastTime: 60, slowSpeed: 20, slowTime: 10, stillTime: 10 },
|
||||
{ id: 'CH6', name: 'CH6', enabled: false, fastSpeed: 100, fastTime: 60, slowSpeed: 20, slowTime: 10, stillTime: 10 }
|
||||
])
|
||||
|
||||
// 页面加载
|
||||
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()
|
||||
})
|
||||
|
||||
// 加载设置
|
||||
const loadSettings = () => {
|
||||
try {
|
||||
const savedData = uni.getStorageSync(STORAGE_KEY)
|
||||
if (savedData) {
|
||||
const data = JSON.parse(savedData)
|
||||
channels.value = data.channels || channels.value
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load settings:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const saveSettings = () => {
|
||||
try {
|
||||
const data = {
|
||||
channels: channels.value,
|
||||
deviceId: deviceId.value,
|
||||
deviceName: deviceName.value,
|
||||
serviceId: serviceId.value,
|
||||
// 保存新协议1.2特征值UUID
|
||||
settingCharId: settingCharId.value,
|
||||
statusCharId: statusCharId.value,
|
||||
controlCharId: controlCharId.value
|
||||
}
|
||||
uni.setStorageSync(STORAGE_KEY, JSON.stringify(data))
|
||||
} catch (e) {
|
||||
console.error('Failed to save settings:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取下拉索引
|
||||
const getFastSpeedIndex = (channelId) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
return fastSpeedValues.indexOf(channel?.fastSpeed || 100)
|
||||
}
|
||||
|
||||
const getFastTimeIndex = (channelId) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
return fastTimeValues.indexOf(channel?.fastTime || 60)
|
||||
}
|
||||
|
||||
const getSlowSpeedIndex = (channelId) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
return slowSpeedValues.indexOf(channel?.slowSpeed || 20)
|
||||
}
|
||||
|
||||
const getSlowTimeIndex = (channelId) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
return slowTimeValues.indexOf(channel?.slowTime || 10)
|
||||
}
|
||||
|
||||
const getStillTimeIndex = (channelId) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
return stillTimeValues.indexOf(channel?.stillTime || 10)
|
||||
}
|
||||
|
||||
// 切换使能状态
|
||||
const toggleEnabled = (channelId) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
if (channel) {
|
||||
channel.enabled = !channel.enabled
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新快速模式转速
|
||||
const updateFastSpeed = (channelId, index) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
if (channel) {
|
||||
channel.fastSpeed = fastSpeedValues[index]
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新快速模式时间
|
||||
const updateFastTime = (channelId, index) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
if (channel) {
|
||||
channel.fastTime = fastTimeValues[index]
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新慢速模式转速
|
||||
const updateSlowSpeed = (channelId, index) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
if (channel) {
|
||||
channel.slowSpeed = slowSpeedValues[index]
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新慢速模式时间
|
||||
const updateSlowTime = (channelId, index) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
if (channel) {
|
||||
channel.slowTime = slowTimeValues[index]
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// 更新静置模式时间
|
||||
const updateStillTime = (channelId, index) => {
|
||||
const channel = channels.value.find(c => c.id === channelId)
|
||||
if (channel) {
|
||||
channel.stillTime = stillTimeValues[index]
|
||||
saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// 返回
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 跳转到启动页面
|
||||
const goToControl = () => {
|
||||
// 先保存设置
|
||||
saveSettings()
|
||||
|
||||
// 构建跳转参数,包含特征值UUID
|
||||
const params = [
|
||||
`deviceId=${deviceId.value}`,
|
||||
`deviceName=${encodeURIComponent(deviceName.value)}`,
|
||||
`serviceId=${serviceId.value}`,
|
||||
`settingCharId=${settingCharId.value}`,
|
||||
`statusCharId=${statusCharId.value}`,
|
||||
`controlCharId=${controlCharId.value}`
|
||||
].join('&')
|
||||
|
||||
// 跳转到控制页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/mixer/control?${params}`
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.setting-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;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: max(calc(44px + env(safe-area-inset-top)), 64px);
|
||||
padding-bottom: 120rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.channel-scroll {
|
||||
flex: 1;
|
||||
padding: 16rpx 20rpx;
|
||||
}
|
||||
|
||||
.channel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.channel-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 2rpx solid #e2e8f0;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.channel-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
// 自定义switch滑块样式
|
||||
.custom-switch {
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
background-color: #cbd5e1;
|
||||
border-radius: 14px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
.switch-slider {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background-color: #667eea;
|
||||
|
||||
.switch-slider {
|
||||
transform: translateX(22px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mode-section {
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.mode-icon {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-row {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
flex: 1;
|
||||
|
||||
.setting-label {
|
||||
font-size: 20rpx;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 4rpx;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
background: #f1f5f9;
|
||||
padding: 8rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #334155;
|
||||
text-align: center;
|
||||
|
||||
&.fixed {
|
||||
background: #e2e8f0;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部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>
|
||||
Binary file not shown.
Reference in New Issue
Block a user