824 lines
20 KiB
Plaintext
824 lines
20 KiB
Plaintext
<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> |