Initial commit of akmon project
This commit is contained in:
824
pages/sense/simulator.uvue
Normal file
824
pages/sense/simulator.uvue
Normal file
@@ -0,0 +1,824 @@
|
||||
<template>
|
||||
<scroll-view direction="vertical" class="simulator-container">
|
||||
<!-- 头部导航 -->
|
||||
<view class="header">
|
||||
<button class="back-btn" @click="goBack">
|
||||
<text class="back-icon">←</text>
|
||||
</button>
|
||||
<text class="title">传感器数据模拟器</text>
|
||||
<button class="clear-btn" @click="clearAllData">清除数据</button>
|
||||
</view>
|
||||
|
||||
<!-- 模拟控制面板 -->
|
||||
<view class="control-panel">
|
||||
<text class="panel-title">模拟控制</text>
|
||||
<view class="control-row">
|
||||
<button class="control-btn" :class="{ active: isSimulating }" @click="toggleSimulation">
|
||||
{{ isSimulating ? '停止模拟' : '开始模拟' }}
|
||||
</button>
|
||||
<text class="status-text">{{ simulationStatus }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 传感器配置 -->
|
||||
<view class="sensor-config">
|
||||
<text class="config-title">传感器配置</text>
|
||||
<view class="sensor-list">
|
||||
<view class="sensor-item" v-for="(sensor, index) in sensorConfigs" :key="index">
|
||||
<view class="sensor-header">
|
||||
<switch :checked="sensor.enabled"
|
||||
@change="onSensorToggle($event as UniSwitchChangeEvent, index)" />
|
||||
<text class="sensor-name">{{ sensor.name }}</text>
|
||||
</view>
|
||||
<view class="sensor-settings" v-if="sensor.enabled">
|
||||
<view class="setting-row">
|
||||
<text class="setting-label">频率</text>
|
||||
<button class="picker-button" @click="showFrequencyPicker(index)">
|
||||
<view class="picker-view">
|
||||
<text>{{ frequencyOptions[sensor.frequency_index] }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
<view class="setting-row">
|
||||
<text class="setting-label">范围</text>
|
||||
<view class="range-inputs">
|
||||
<input class="range-input" type="number" v-model="sensor.min_value" placeholder="最小值" />
|
||||
<text class="range-separator">-</text>
|
||||
<input class="range-input" type="number" v-model="sensor.max_value" placeholder="最大值" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="setting-row">
|
||||
<text class="setting-label">变化趋势</text>
|
||||
<button class="picker-button" @click="showTrendPicker(index)">
|
||||
<view class="picker-view">
|
||||
<text>{{ trendOptions[sensor.trend_index] }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<view class="stats-section">
|
||||
<text class="stats-title">生成统计</text>
|
||||
<view class="stats-grid">
|
||||
<view class="stats-item">
|
||||
<text class="stats-label">已生成数据</text>
|
||||
<text class="stats-value">{{ generatedCount }}</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="stats-label">运行时间</text>
|
||||
<text class="stats-value">{{ runningTime }}</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="stats-label">数据速率</text>
|
||||
<text class="stats-value">{{ dataRate }}/秒</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 实时数据预览 -->
|
||||
<view class="preview-section">
|
||||
<text class="preview-title">实时数据预览</text>
|
||||
<scroll-view class="preview-list" scroll-y>
|
||||
<view class="preview-item" v-for="(item, index) in recentData" :key="index">
|
||||
<view class="preview-header">
|
||||
<text class="preview-type">{{ item.type }}</text>
|
||||
<text class="preview-time">{{ formatTime(item.timestamp) }}</text>
|
||||
</view>
|
||||
<text class="preview-value">{{ item.value }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 批量生成 -->
|
||||
<view class="batch-section">
|
||||
<text class="batch-title">批量生成</text>
|
||||
<view class="batch-form">
|
||||
<view class="form-row">
|
||||
<text class="form-label">数据量</text>
|
||||
<input class="form-input" type="number" v-model="batchCount" placeholder="请输入数据量" />
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">时间跨度</text>
|
||||
<view class="picker-view" @click="showTimeSpanPicker">
|
||||
<text>{{ timeSpanOptions[timeSpanIndex] }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</view>
|
||||
<button class="batch-btn" @click="generateBatchData">生成批量数据</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
import type { SensorConfig, RecentDataItem, SensorMeasurement } from './types.uts'
|
||||
|
||||
// 工具函数,放在 import 之后,所有变量和业务逻辑前
|
||||
function getFrequencySeconds(index : number) : number {
|
||||
// 频率选项:['1秒', '5秒', '10秒', '30秒', '1分钟', '5分钟']
|
||||
switch (index) {
|
||||
case 0: return 1;
|
||||
case 1: return 5;
|
||||
case 2: return 10;
|
||||
case 3: return 30;
|
||||
case 4: return 60;
|
||||
case 5: return 300;
|
||||
default: return 10;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function getUnit(sensorKey : string) : string {
|
||||
switch (sensorKey) {
|
||||
case 'heart_rate': return 'bpm'
|
||||
case 'steps': return '步'
|
||||
case 'spo2': return '%'
|
||||
case 'temp': return '°C'
|
||||
case 'bp': return 'mmHg'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
function formatPreviewValue(sensorKey : string, value : number) : string {
|
||||
switch (sensorKey) {
|
||||
case 'heart_rate': return `${Math.round(value)} bpm`
|
||||
case 'steps': return `${Math.round(value)} 步`
|
||||
case 'spo2': return `${Math.round(value)} %`
|
||||
case 'temp': return `${(Math.round(value * 10) / 10).toFixed(1)} 度`
|
||||
case 'bp': return `${Math.round(value)}/${Math.round(value * 0.7)} mmHg`
|
||||
default: return value.toString()
|
||||
}
|
||||
}
|
||||
|
||||
function getTimeSpanHours(index : number) : number {
|
||||
// ['1小时', '1天', '1周', '1个月']
|
||||
switch (index) {
|
||||
case 0: return 1;
|
||||
case 1: return 24;
|
||||
case 2: return 24 * 7;
|
||||
case 3: return 24 * 30;
|
||||
default: return 24;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const isSimulating = ref<boolean>(false)
|
||||
const simulationStatus = ref<string>('已停止')
|
||||
const generatedCount = ref<number>(0)
|
||||
const runningTime = ref<string>('00:00:00')
|
||||
const dataRate = ref<number>(0)
|
||||
const recentData = ref<RecentDataItem[]>([])
|
||||
const batchCount = ref<string>('100')
|
||||
const timeSpanIndex = ref<number>(1)
|
||||
|
||||
// 传感器配置
|
||||
const sensorConfigs = ref<SensorConfig[]>([
|
||||
{
|
||||
key: 'heart_rate',
|
||||
name: '心率',
|
||||
enabled: true,
|
||||
frequency_index: 2,
|
||||
min_value: '60',
|
||||
max_value: '100',
|
||||
trend_index: 0
|
||||
},
|
||||
{
|
||||
key: 'steps',
|
||||
name: '步数',
|
||||
enabled: true,
|
||||
frequency_index: 3,
|
||||
min_value: '0',
|
||||
max_value: '50',
|
||||
trend_index: 1
|
||||
},
|
||||
{
|
||||
key: 'spo2',
|
||||
name: '血氧',
|
||||
enabled: false,
|
||||
frequency_index: 4,
|
||||
min_value: '95',
|
||||
max_value: '100',
|
||||
trend_index: 0
|
||||
},
|
||||
{
|
||||
key: 'temp',
|
||||
name: '体温',
|
||||
enabled: false,
|
||||
frequency_index: 5,
|
||||
min_value: '36.0',
|
||||
max_value: '37.5',
|
||||
trend_index: 0
|
||||
},
|
||||
{
|
||||
key: 'bp',
|
||||
name: '血压',
|
||||
enabled: false,
|
||||
frequency_index: 5,
|
||||
min_value: '90',
|
||||
max_value: '140',
|
||||
trend_index: 0
|
||||
}
|
||||
])
|
||||
|
||||
// 选项数据
|
||||
const frequencyOptions = ['1秒', '5秒', '10秒', '30秒', '1分钟', '5分钟']
|
||||
const trendOptions = ['随机', '上升', '下降', '波动']
|
||||
const timeSpanOptions = ['1小时', '1天', '1周', '1个月']
|
||||
|
||||
const userId = 'eed3824b-bba1-4309-8048-19d17367c084'
|
||||
const deviceId = '12345678-1234-5678-9abc-123456789012'
|
||||
let simulationTimer : number | null = null
|
||||
let startTime : number = 0
|
||||
function generateValueWithTrend(min : number, max : number, trend : number) : number {
|
||||
let value : number
|
||||
if (trend === 0) { // 随机
|
||||
value = min + Math.random() * (max - min)
|
||||
} else if (trend === 1) { // 上升
|
||||
const progress = (generatedCount.value % 100) / 100
|
||||
value = min + progress * (max - min) + Math.random() * (max - min) * 0.1
|
||||
} else if (trend === 2) { // 下降
|
||||
const progress = 1 - (generatedCount.value % 100) / 100
|
||||
value = min + progress * (max - min) + Math.random() * (max - min) * 0.1
|
||||
} else { // 波动
|
||||
const wave = Math.sin((generatedCount.value % 100) / 100 * Math.PI * 2)
|
||||
value = (min + max) / 2 + wave * (max - min) / 4 + Math.random() * (max - min) * 0.1
|
||||
}
|
||||
return Math.max(min, Math.min(max, value))
|
||||
}
|
||||
|
||||
async function generateBatchSensorData(sensor : SensorConfig, timestamp : Date) {
|
||||
if (supa === null) return
|
||||
|
||||
const sensorKey = sensor.key
|
||||
const minValue = parseFloat(sensor.min_value)
|
||||
const maxValue = parseFloat(sensor.max_value)
|
||||
|
||||
const value = minValue + Math.random() * (maxValue - minValue)
|
||||
const rawData = new UTSJSONObject()
|
||||
|
||||
// 根据传感器类型构建原始数据
|
||||
if (sensorKey === 'heart_rate') {
|
||||
rawData.set('bpm', Math.round(value))
|
||||
} else if (sensorKey === 'steps') {
|
||||
rawData.set('count', Math.round(value))
|
||||
} else if (sensorKey === 'spo2') {
|
||||
rawData.set('spo2', Math.round(value))
|
||||
} else if (sensorKey === 'temp') {
|
||||
rawData.set('temp', Math.round(value * 10) / 10)
|
||||
} else if (sensorKey === 'bp') {
|
||||
rawData.set('systolic', Math.round(value))
|
||||
rawData.set('diastolic', Math.round(value * 0.7))
|
||||
}
|
||||
|
||||
const measurementData = new UTSJSONObject()
|
||||
measurementData.set('device_id', deviceId)
|
||||
measurementData.set('user_id', userId)
|
||||
measurementData.set('measurement_type', sensorKey)
|
||||
measurementData.set('measured_at', timestamp.toISOString())
|
||||
measurementData.set('unit', getUnit(sensorKey))
|
||||
measurementData.set('raw_data', rawData)
|
||||
|
||||
await supa.from('ss_sensor_measurements')
|
||||
.insert(measurementData)
|
||||
.execute()
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function generateBatchData() {
|
||||
if (supa === null) return
|
||||
|
||||
const count = parseInt(batchCount.value) ?? 100
|
||||
const timeSpan = getTimeSpanHours(timeSpanIndex.value)
|
||||
|
||||
uni.showLoading({
|
||||
title: '生成批量数据中...'
|
||||
})
|
||||
|
||||
try {
|
||||
const endTime = new Date()
|
||||
const startTime = new Date(endTime.getTime() - timeSpan * 3600000)
|
||||
const interval = (timeSpan * 3600000) / count
|
||||
|
||||
for (let i : Int = 0; i < count; i++) {
|
||||
const timestamp = new Date(startTime.getTime() + i * interval)
|
||||
|
||||
const enabledSensors = sensorConfigs.value.filter(sensor => sensor.enabled)
|
||||
for (let j : Int = 0; j < enabledSensors.length; j++) {
|
||||
const sensor = enabledSensors[j]
|
||||
await generateBatchSensorData(sensor, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '批量数据生成完成',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (e) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '生成失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAllData() {
|
||||
uni.showModal({
|
||||
title: '确认清除',
|
||||
content: '确定要清除所有模拟数据吗?此操作不可恢复!',
|
||||
success: function (res) {
|
||||
if (res.confirm && supa !== null) {
|
||||
(async () => {
|
||||
try {
|
||||
await supa.from('ss_sensor_measurements')
|
||||
.delete()
|
||||
.eq('device_id', deviceId)
|
||||
.execute()
|
||||
|
||||
generatedCount.value = 0
|
||||
recentData.value = []
|
||||
|
||||
uni.showToast({
|
||||
title: '数据清除完成',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (e) {
|
||||
uni.showToast({
|
||||
title: '清除失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
// 更新运行时间
|
||||
const elapsed = Date.now() - startTime
|
||||
const hours = Math.floor(elapsed / 3600000)
|
||||
const minutes = Math.floor((elapsed % 3600000) / 60000)
|
||||
const seconds = Math.floor((elapsed % 60000) / 1000)
|
||||
runningTime.value = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||
|
||||
// 更新数据速率
|
||||
if (elapsed > 0) {
|
||||
dataRate.value = Math.round((generatedCount.value / elapsed) * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 事件处理
|
||||
function onSensorToggle(e : UniSwitchChangeEvent, index : number) {
|
||||
if (index >= 0 && index < sensorConfigs.value.length) {
|
||||
sensorConfigs.value[index].enabled = e.detail.value
|
||||
}
|
||||
}
|
||||
|
||||
function onFrequencyChange(e : number, index : number) {
|
||||
if (index >= 0 && index < sensorConfigs.value.length) {
|
||||
sensorConfigs.value[index].frequency_index = e
|
||||
}
|
||||
}
|
||||
|
||||
function onTrendChange(e : number, index : number) {
|
||||
if (index >= 0 && index < sensorConfigs.value.length) {
|
||||
sensorConfigs.value[index].trend_index = e
|
||||
}
|
||||
}
|
||||
|
||||
// ActionSheet handlers
|
||||
function showFrequencyPicker(index : number) {
|
||||
uni.showActionSheet({
|
||||
itemList: frequencyOptions,
|
||||
success: (res) => {
|
||||
if (res.tapIndex >= 0) {
|
||||
onFrequencyChange(res.tapIndex, index)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showTrendPicker(index : number) {
|
||||
uni.showActionSheet({
|
||||
itemList: trendOptions, // FIXED: removed .value
|
||||
success: (res) => {
|
||||
if (res.tapIndex >= 0) {
|
||||
onTrendChange(res.tapIndex, index)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showTimeSpanPicker() {
|
||||
uni.showActionSheet({
|
||||
itemList: timeSpanOptions,
|
||||
success: (res) => {
|
||||
if (res.tapIndex >= 0) {
|
||||
timeSpanIndex.value = res.tapIndex
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function generateSensorData(sensor : SensorConfig) {
|
||||
if (supa === null) return
|
||||
|
||||
const sensorKey = sensor.key
|
||||
const minValue = parseFloat(sensor.min_value)
|
||||
const maxValue = parseFloat(sensor.max_value)
|
||||
const trend = sensor.trend_index
|
||||
|
||||
let value = generateValueWithTrend(minValue, maxValue, trend)
|
||||
const rawData = new UTSJSONObject()
|
||||
|
||||
// 根据传感器类型构建原始数据
|
||||
if (sensorKey === 'heart_rate') {
|
||||
rawData.set('bpm', Math.round(value))
|
||||
rawData.set('rr_interval', Math.round(60000 / value))
|
||||
} else if (sensorKey === 'steps') {
|
||||
rawData.set('count', Math.round(value))
|
||||
rawData.set('distance', Math.round(value * 0.7))
|
||||
} else if (sensorKey === 'spo2') {
|
||||
rawData.set('spo2', Math.round(value))
|
||||
rawData.set('pi', Math.round(Math.random() * 10) / 10)
|
||||
} else if (sensorKey === 'temp') {
|
||||
rawData.set('temp', Math.round(value * 10) / 10)
|
||||
} else if (sensorKey === 'bp') {
|
||||
rawData.set('systolic', Math.round(value))
|
||||
rawData.set('diastolic', Math.round(value * 0.7))
|
||||
}
|
||||
|
||||
// 构建测量数据
|
||||
const measurementData = new UTSJSONObject()
|
||||
measurementData.set('device_id', deviceId)
|
||||
measurementData.set('user_id', userId)
|
||||
measurementData.set('measurement_type', sensorKey)
|
||||
measurementData.set('measured_at', new Date().toISOString())
|
||||
measurementData.set('unit', getUnit(sensorKey))
|
||||
measurementData.set('raw_data', rawData)
|
||||
|
||||
try {
|
||||
await supa.from('ss_sensor_measurements')
|
||||
.insert(measurementData)
|
||||
.execute()
|
||||
|
||||
// 添加到预览列表
|
||||
const previewItem : RecentDataItem = {
|
||||
type: sensor.name,
|
||||
value: formatPreviewValue(sensorKey, value),
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
recentData.value.unshift(previewItem)
|
||||
if (recentData.value.length > 20) {
|
||||
recentData.value = recentData.value.slice(0, 20)
|
||||
}
|
||||
|
||||
generatedCount.value++
|
||||
} catch (e) {
|
||||
console.log('生成数据失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function generateRealtimeData() {
|
||||
const enabledSensors = sensorConfigs.value.filter(sensor => sensor.enabled)
|
||||
|
||||
for (let i : Int = 0; i < enabledSensors.length; i++) {
|
||||
const sensor = enabledSensors[i]
|
||||
const frequency = getFrequencySeconds(sensor.frequency_index)
|
||||
|
||||
// 根据频率决定是否生成数据
|
||||
if (generatedCount.value % frequency === 0) {
|
||||
await generateSensorData(sensor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function startSimulation() {
|
||||
isSimulating.value = true
|
||||
simulationStatus.value = '运行中'
|
||||
startTime = Date.now()
|
||||
generatedCount.value = 0
|
||||
|
||||
// 开始模拟定时器
|
||||
simulationTimer = setInterval(() => {
|
||||
generateRealtimeData()
|
||||
updateStats()
|
||||
}, 1000) // 每秒生成一次数据
|
||||
}
|
||||
|
||||
function stopSimulation() {
|
||||
isSimulating.value = false
|
||||
simulationStatus.value = '已停止'
|
||||
|
||||
if (simulationTimer !== null) {
|
||||
clearInterval(simulationTimer as number) // FIXED: type assertion
|
||||
simulationTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function formatTime(timestamp : number) : string {
|
||||
const date = new Date(timestamp)
|
||||
const y = date.getFullYear()
|
||||
const m = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const d = date.getDate().toString().padStart(2, '0')
|
||||
const h = date.getHours().toString().padStart(2, '0')
|
||||
const min = date.getMinutes().toString().padStart(2, '0')
|
||||
const s = date.getSeconds().toString().padStart(2, '0')
|
||||
return `${y}-${m}-${d} ${h}:${min}:${s}`
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
onMounted(() => {
|
||||
// supa 已全局初始化,无需手动实例化
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopSimulation()
|
||||
})
|
||||
|
||||
function toggleSimulation() {
|
||||
if (isSimulating.value) {
|
||||
stopSimulation()
|
||||
} else {
|
||||
startSimulation()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.simulator-container {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 20rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
padding: 12rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 8rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 32rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
padding: 16rpx 24rpx;
|
||||
background-color: #F56C6C;
|
||||
color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.control-panel,
|
||||
.sensor-config,
|
||||
.stats-section,
|
||||
.preview-section,
|
||||
.batch-section {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 24rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.panel-title,
|
||||
.config-title,
|
||||
.stats-title,
|
||||
.preview-title,
|
||||
.batch-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.control-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 20rpx 40rpx;
|
||||
background-color: #409EFF;
|
||||
color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.control-btn.active {
|
||||
background-color: #F56C6C;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.sensor-list {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sensor-item {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 20rpx;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.sensor-header {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.sensor-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.sensor-settings {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
width: 120rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16rpx 20rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
min-width: 160rpx;
|
||||
}
|
||||
|
||||
.picker-button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
color: #999999;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.range-inputs {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.range-input {
|
||||
flex: 1;
|
||||
padding: 16rpx;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.range-separator {
|
||||
margin: 0 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.preview-list {
|
||||
max-height: 400rpx;
|
||||
}
|
||||
|
||||
.preview-item {
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.preview-type {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.preview-time {
|
||||
font-size: 22rpx;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.preview-value {
|
||||
font-size: 28rpx;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.batch-form {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
width: 150rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
padding: 20rpx;
|
||||
border: 2rpx solid #e0e0e0;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.batch-btn {
|
||||
padding: 24rpx;
|
||||
background-color: #67C23A;
|
||||
color: #ffffff;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user