1 Commits
English ... dev

Author SHA1 Message Date
tianyongbao
0bb5650cf6 fix: 修改逻辑。 2025-12-22 12:30:07 +08:00
12 changed files with 481 additions and 3008 deletions

5
.gitignore vendored
View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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>