Initial commit of akmon project
This commit is contained in:
315
pages/test/ap_monitor.uvue
Normal file
315
pages/test/ap_monitor.uvue
Normal file
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="nav-tabs">
|
||||
<button class="nav-tab active">智能网关</button>
|
||||
<button class="nav-tab" @click="switchPage">设备监测</button>
|
||||
</view>
|
||||
<view class="header">
|
||||
<text class="title">智能网关状态监测</text>
|
||||
<text class="subtitle">总数: {{ nodes.length }} | 在线: {{ onlineCount }}</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="grid-container" scroll-y="true">
|
||||
<view class="grid-layout">
|
||||
<view v-for="node in nodes" :key="node.id" class="card" :class="isOnline(node) ? 'card-online' : 'card-offline'">
|
||||
<view class="card-header">
|
||||
<text class="card-title">{{ node.name }}</text>
|
||||
<view class="status-badge" :class="isOnline(node) ? 'badge-online' : 'badge-offline'">
|
||||
<text class="badge-text">{{ isOnline(node) ? '在线' : '离线' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<view class="info-row">
|
||||
<text class="label">网关ID:</text>
|
||||
<text class="value">{{ node.mqtt_client_id }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">区域:</text>
|
||||
<text class="value">{{ node.region ?? '演示区域' }}</text>
|
||||
</view>
|
||||
<!-- <view class="info-row">
|
||||
<text class="label">版本:</text>
|
||||
<text class="value">{{ node.version ?? '-' }}</text>
|
||||
</view> -->
|
||||
<view class="info-row">
|
||||
<text class="label">最后更新:</text>
|
||||
<text class="value time-value">{{ formatTime(node.updated_at) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">更新时间:</text>
|
||||
<text class="value">{{ getTimeAgo(node.updated_at) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import GatewayRealtimeService, { GatewayNode, GatewayRealtimeSubscription } from '@/utils/gatewayRealtimeService.uts'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
nodes: [] as GatewayNode[],
|
||||
subscription: null as GatewayRealtimeSubscription | null,
|
||||
now: Date.now(),
|
||||
timer: null as number | null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
onlineCount(): number {
|
||||
let count = 0
|
||||
for (let i = 0; i < this.nodes.length; i++) {
|
||||
if (this.isOnline(this.nodes[i])) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.startMonitoring()
|
||||
this.timer = setInterval(() => {
|
||||
this.now = Date.now()
|
||||
}, 1000)
|
||||
},
|
||||
onUnload() {
|
||||
this.stopMonitoring()
|
||||
if (this.timer != null) {
|
||||
clearInterval(this.timer!)
|
||||
this.timer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async startMonitoring() {
|
||||
this.subscription = await GatewayRealtimeService.subscribeGateways({
|
||||
onInitial: (initialNodes: GatewayNode[]) => {
|
||||
console.log('Initial nodes loaded:', initialNodes.length)
|
||||
this.nodes = initialNodes
|
||||
},
|
||||
onInsert: (node: GatewayNode) => {
|
||||
console.log('AP Node Inserted:', node.name)
|
||||
this.nodes.push(node)
|
||||
},
|
||||
onUpdate: (node: GatewayNode) => {
|
||||
console.log('AP Node Updated:', node.name, node.updated_at)
|
||||
const index = this.nodes.findIndex((n) => n.id == node.id)
|
||||
if (index >= 0) {
|
||||
// 使用 splice 确保视图能够响应数组元素的变化
|
||||
this.nodes.splice(index, 1, node)
|
||||
} else {
|
||||
// 如果列表中没有该节点(可能是初始加载遗漏或新节点),则添加
|
||||
console.log('Node not found in list, adding it:', node.name)
|
||||
this.nodes.push(node)
|
||||
}
|
||||
},
|
||||
onDelete: (id: string) => {
|
||||
const index = this.nodes.findIndex((n) => n.id == id)
|
||||
if (index >= 0) {
|
||||
this.nodes.splice(index, 1)
|
||||
}
|
||||
},
|
||||
onError: (err: any) => {
|
||||
console.error('Gateway monitoring error', err)
|
||||
uni.showToast({
|
||||
title: '连接失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
stopMonitoring() {
|
||||
if (this.subscription != null) {
|
||||
this.subscription!.dispose()
|
||||
this.subscription = null
|
||||
}
|
||||
},
|
||||
isOnline(node: GatewayNode): boolean {
|
||||
const updateTime = new Date(node.updated_at).getTime()
|
||||
// 60 seconds timeout
|
||||
return (this.now - updateTime) < 60000
|
||||
},
|
||||
formatTime(isoString: string): string {
|
||||
const date = new Date(isoString)
|
||||
return date.toLocaleTimeString()
|
||||
},
|
||||
getTimeAgo(isoString: string): string {
|
||||
const updateTime = new Date(isoString).getTime()
|
||||
const diff = Math.floor((this.now - updateTime) / 1000)
|
||||
if (diff < 0) return '刚刚'
|
||||
if (diff < 60) return `${diff}秒前`
|
||||
return `${Math.floor(diff / 60)}分前`
|
||||
},
|
||||
switchPage() {
|
||||
uni.redirectTo({
|
||||
url: '/pages/test/multi_device_monitor'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
margin: 0 5px;
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background-color: #007aff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 15px 20px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.grid-layout {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 48%;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 15px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
.card-online {
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
|
||||
.card-offline {
|
||||
border-left-color: #9e9e9e;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.badge-online {
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
.badge-offline {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge-online .badge-text {
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.badge-offline .badge-text {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
338
pages/test/device_monitor.uvue
Normal file
338
pages/test/device_monitor.uvue
Normal file
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="nav-tabs">
|
||||
<button class="nav-tab" @click="switchPage">AP网关</button>
|
||||
<button class="nav-tab active">设备监测</button>
|
||||
</view>
|
||||
<view class="header">
|
||||
<text class="title">设备实时数据监测</text>
|
||||
</view>
|
||||
|
||||
<!-- <view class="input-group">
|
||||
<view class="input-item">
|
||||
<text class="label">Room ID:</text>
|
||||
<input class="input" v-model="roomId" placeholder="例如 room:0001" />
|
||||
</view>
|
||||
<view class="input-item">
|
||||
<text class="label">Device ID:</text>
|
||||
<input class="input" type="number" v-model="deviceId" placeholder="例如 11" />
|
||||
</view>
|
||||
<button class="btn" @click="toggleSubscription">{{ isSubscribing ? '停止监测' : '开始监测' }}</button>
|
||||
</view> -->
|
||||
|
||||
<view class="input-group" style="margin-top: 10px;">
|
||||
<text class="label" style="width: 100%; margin-bottom: 5px;">测试下行指令 (Test Downlink)</text>
|
||||
<view class="input-item">
|
||||
<text class="label">AP ID:</text>
|
||||
<input class="input" v-model="apId" placeholder="例如 WDDGW20000014" />
|
||||
</view>
|
||||
<view class="input-item">
|
||||
<text class="label">CMD:</text>
|
||||
<input class="input" type="number" v-model="cmd" placeholder="例如 28689" />
|
||||
</view>
|
||||
<view class="input-item">
|
||||
<text class="label">Dev ID:</text>
|
||||
<input class="input" v-model="devIdInput" placeholder="例如 [11]" />
|
||||
</view>
|
||||
<button class="btn" @click="sendTestDownlink">发送测试指令</button>
|
||||
</view>
|
||||
|
||||
<view class="status-panel">
|
||||
<text class="status-text">状态: {{ status }}</text>
|
||||
<text class="status-text">最后更新: {{ lastUpdateTime }}</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="log-container" scroll-y="true">
|
||||
<view v-for="(log, index) in logs" :key="index" class="log-item">
|
||||
<text class="log-text">{{ log }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="currentData != null" class="data-card">
|
||||
<view class="card-row">
|
||||
<text class="card-label">心率 (HeartRate):</text>
|
||||
<text class="card-value highlight">{{ currentData!.heartrate }} bpm</text>
|
||||
</view>
|
||||
<view class="card-row">
|
||||
<text class="card-label">接收时间 (RecvTime):</text>
|
||||
<text class="card-value">{{ formatTime(currentData!.recvtime) }}</text>
|
||||
</view>
|
||||
<view class="card-row">
|
||||
<text class="card-label">信号 (RSSI):</text>
|
||||
<text class="card-value">{{ currentData!.rssi }}</text>
|
||||
</view>
|
||||
<view class="card-row">
|
||||
<text class="card-label">活动等级:</text>
|
||||
<text class="card-value">{{ currentData!.activitylevel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import DeviceRealtimeService, { DeviceBatchItem, DeviceRealtimeSubscription } from '@/utils/deviceRealtimeService.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
roomId: 'room:0001',
|
||||
deviceId: 11,
|
||||
devIdInput: '[11]',
|
||||
apId: 'WDDGW20000014',
|
||||
cmd: 28689,
|
||||
isSubscribing: false,
|
||||
status: '未连接',
|
||||
logs: [] as string[],
|
||||
currentData: null as DeviceBatchItem | null,
|
||||
subscription: null as DeviceRealtimeSubscription | null,
|
||||
lastUpdateTime: '-'
|
||||
}
|
||||
},
|
||||
onLoad(options:OnLoadOptions) {
|
||||
let idStr: string | null = null
|
||||
if (options['id']) {
|
||||
idStr = options['id']
|
||||
} else if (options['deviceId']) {
|
||||
idStr = options['deviceId']
|
||||
}
|
||||
|
||||
if (idStr != null) {
|
||||
this.deviceId = parseInt(idStr)
|
||||
this.devIdInput = `[${idStr}]`
|
||||
// 自动生成测试用的 AP ID: WDDGW + (20000000 + deviceId)
|
||||
this.apId = 'WDDGW' + (20000000 + this.deviceId).toString()
|
||||
this.startSubscription()
|
||||
}
|
||||
},
|
||||
onUnload() {
|
||||
this.stopSubscription()
|
||||
},
|
||||
methods: {
|
||||
toggleSubscription() {
|
||||
if (this.isSubscribing) {
|
||||
this.stopSubscription()
|
||||
} else {
|
||||
this.startSubscription()
|
||||
}
|
||||
},
|
||||
async startSubscription() {
|
||||
if (this.roomId == '') {
|
||||
this.addLog('错误: Room ID 不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
this.isSubscribing = true
|
||||
this.status = '正在连接...'
|
||||
this.logs = []
|
||||
this.addLog(`开始订阅 Room: ${this.roomId}, Device: ${this.deviceId}`)
|
||||
|
||||
try {
|
||||
this.subscription = await DeviceRealtimeService.subscribeDevice({
|
||||
roomId: this.roomId,
|
||||
deviceId: Number(this.deviceId)
|
||||
}, {
|
||||
onData: (data: DeviceBatchItem) => {
|
||||
this.currentData = data
|
||||
this.lastUpdateTime = new Date().toLocaleTimeString()
|
||||
this.status = '接收数据中'
|
||||
this.addLog(`收到数据: HR=${data.heartrate}, Time=${data.recvtime}`)
|
||||
},
|
||||
onError: (err: any) => {
|
||||
this.status = '错误'
|
||||
this.addLog(`订阅错误: ${err}`)
|
||||
this.isSubscribing = false
|
||||
}
|
||||
})
|
||||
this.status = '已订阅 (等待数据)'
|
||||
} catch (e) {
|
||||
this.status = '连接失败'
|
||||
this.addLog(`连接异常: ${e}`)
|
||||
this.isSubscribing = false
|
||||
}
|
||||
},
|
||||
stopSubscription() {
|
||||
if (this.subscription != null) {
|
||||
this.subscription!.dispose()
|
||||
this.subscription = null
|
||||
}
|
||||
this.isSubscribing = false
|
||||
this.status = '已停止'
|
||||
this.addLog('停止订阅')
|
||||
},
|
||||
addLog(msg: string) {
|
||||
const time = new Date().toLocaleTimeString()
|
||||
this.logs.unshift(`[${time}] ${msg}`)
|
||||
if (this.logs.length > 50) {
|
||||
this.logs.pop()
|
||||
}
|
||||
},
|
||||
formatTime(ts: number): string {
|
||||
return new Date(ts).toLocaleTimeString()
|
||||
},
|
||||
switchPage() {
|
||||
uni.redirectTo({
|
||||
url: '/pages/test/ap_monitor'
|
||||
})
|
||||
},
|
||||
async sendTestDownlink() {
|
||||
if (this.apId == '') {
|
||||
this.addLog('错误: AP ID 不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
const topic = `watch/server/data/${this.apId}`
|
||||
const payload = {
|
||||
cmd: Number(this.cmd),
|
||||
token: Math.floor(Math.random() * 100000),
|
||||
timeout: 2,
|
||||
inval: 1,
|
||||
devid: JSON.parse(this.devIdInput)
|
||||
}
|
||||
|
||||
this.addLog(`正在发送下行指令... Topic: ${topic}`)
|
||||
|
||||
const currentUser = supa.user
|
||||
const userId = currentUser?.getString('id') ?? '00000000-0000-0000-0000-000000000000'
|
||||
|
||||
try {
|
||||
const res = await supa.from('chat_mqtt_downlinks').insert({
|
||||
topic: topic,
|
||||
payload: payload,
|
||||
qos: 1,
|
||||
conversation_id: '00000000-0000-0000-0000-000000000000',
|
||||
created_by: userId
|
||||
}).execute()
|
||||
|
||||
if (res.error != null) {
|
||||
this.addLog(`发送失败: ${res.error}`)
|
||||
} else {
|
||||
this.addLog('发送成功! 数据已写入 chat_mqtt_downlinks')
|
||||
}
|
||||
} catch (e) {
|
||||
this.addLog(`发送异常: ${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff;
|
||||
padding: 0 0 10px 0;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
margin: 0 5px;
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background-color: #007aff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.input-group {
|
||||
background-color: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.input-item {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.label {
|
||||
width: 80px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.input {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.btn {
|
||||
margin-top: 10px;
|
||||
background-color: #007aff;
|
||||
color: #fff;
|
||||
}
|
||||
.status-panel {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.status-text {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
.data-card {
|
||||
background-color: #e3f2fd;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #90caf9;
|
||||
}
|
||||
.card-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.card-label {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
.card-value {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #1565c0;
|
||||
}
|
||||
.highlight {
|
||||
font-size: 24px;
|
||||
color: #d32f2f;
|
||||
}
|
||||
.log-container {
|
||||
flex: 1;
|
||||
background-color: #333;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
.log-item {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.log-text {
|
||||
color: #00e676;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
114
pages/test/gender-picker-view-test.uvue
Normal file
114
pages/test/gender-picker-view-test.uvue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<view>
|
||||
<button @click="openPicker">选择性别</button>
|
||||
<view v-if="showPicker" class="picker-modal">
|
||||
<picker-view
|
||||
class="picker-view"
|
||||
:value="tempGenderIndex"
|
||||
:indicator-style="'height: 50px;'"
|
||||
@change="onPickerChange"
|
||||
>
|
||||
<picker-view-column>
|
||||
<view v-for="(g, idx) in genderOptions" :key="g" class="picker-item">{{ g }}</view>
|
||||
</picker-view-column>
|
||||
</picker-view>
|
||||
<view class="picker-actions">
|
||||
<button @click="showPicker = false">取消</button>
|
||||
<button @click="confirmGender">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
genderOptions: ['male', 'female', 'other'],
|
||||
tempGenderIndex: [0], // 作为 number[]
|
||||
selectedGenderIndex: 0,
|
||||
showPicker: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openPicker() {
|
||||
// 确保弹窗显示前索引合<E5BC95>?
|
||||
const idx = (this.selectedGenderIndex >= 0 && this.selectedGenderIndex < this.genderOptions.length)
|
||||
? this.selectedGenderIndex : 0;
|
||||
this.tempGenderIndex = [idx];
|
||||
this.showPicker = true;
|
||||
},
|
||||
onPickerChange(e: UniPickerViewChangeEvent) {
|
||||
const idx = e.detail.value[0];
|
||||
// 兜底保护,防止越<E6ADA2>?
|
||||
this.tempGenderIndex = [(idx >= 0 && idx < this.genderOptions.length) ? idx : 0];
|
||||
},
|
||||
confirmGender() {
|
||||
this.selectedGenderIndex = this.tempGenderIndex[0];
|
||||
this.showPicker = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.picker-modal {
|
||||
position: fixed;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
background: #fff;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 -2px 20px rgba(0,0,0,0.1);
|
||||
padding-bottom: 30rpx;
|
||||
width: 100vw;
|
||||
border-top-left-radius: 20rpx;
|
||||
border-top-right-radius: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.picker-view {
|
||||
width: 100%;
|
||||
min-width: 240px;
|
||||
max-width: 100vw;
|
||||
height: 320px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.picker-view-column {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.picker-item {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.picker-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100vw;
|
||||
padding: 20rpx 40rpx 0 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.picker-actions button {
|
||||
flex: 1;
|
||||
margin: 0 10rpx;
|
||||
background: #2196f3;
|
||||
color: #fff;
|
||||
border-radius: 10rpx;
|
||||
font-size: 28rpx;
|
||||
height: 80rpx;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
415
pages/test/multi_device_monitor.uvue
Normal file
415
pages/test/multi_device_monitor.uvue
Normal file
@@ -0,0 +1,415 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="nav-tabs">
|
||||
<button class="nav-tab" @click="switchPage">AP网关</button>
|
||||
<button class="nav-tab active">设备监测</button>
|
||||
</view>
|
||||
<view class="header">
|
||||
<text class="title">设备实时数据监测</text>
|
||||
<text class="subtitle">总数: {{ allDevices.length }} | 在线: {{ onlineCount }}</text>
|
||||
</view>
|
||||
|
||||
<scroll-view class="grid-container" scroll-y="true">
|
||||
<view class="grid-layout">
|
||||
<view v-for="device in allDevices" :key="device.uniqueKey" class="card" :class="device.isOnline ? 'card-online' : 'card-offline'" @click="openDeviceTest(device)">
|
||||
<view class="card-header">
|
||||
<text class="card-title">{{ device.name }} ({{ device.numericId > 0 ? device.numericId.toString(16).toUpperCase() : '' }})</text>
|
||||
<view class="status-badge" :class="device.isOnline ? 'badge-online' : 'badge-offline'">
|
||||
<text class="badge-text">{{ device.isOnline ? '在线' : '离线' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="card-body">
|
||||
<view class="info-row">
|
||||
<text class="label">WatchID:</text>
|
||||
<text class="value">{{ device.numericId > 0 ? device.numericId : '' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">MAC:</text>
|
||||
<text class="value">{{ device.mac }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">心率:</text>
|
||||
<text class="value highlight">{{ device.data?.heartrate ?? '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">RSSI:</text>
|
||||
<text class="value">{{ device.data?.rssi ?? '-' }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="label">最后更新:</text>
|
||||
<text class="value time-value">{{ formatTime(device.lastUpdateTime) }}</text>
|
||||
</view>
|
||||
<!-- <view class="action-row" style="margin-top: 8px; display: flex; justify-content: flex-end;">
|
||||
<button size="mini" type="primary" style="font-size: 12px; margin: 0;" @click.stop="openDeviceTest(device)">测试</button>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import DeviceRealtimeService, { DeviceBatchItem, DeviceRealtimeSubscription } from '@/utils/deviceRealtimeService.uts'
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
type DisplayDevice = {
|
||||
uniqueKey: string
|
||||
dbId: string | null
|
||||
numericId: number
|
||||
name: string
|
||||
mac: string
|
||||
isOnline: boolean
|
||||
lastUpdateTime: number
|
||||
data: DeviceBatchItem | null
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
devicesMap: new Map<string, DisplayDevice>(),
|
||||
subscription: null as DeviceRealtimeSubscription | null,
|
||||
now: Date.now(),
|
||||
timer: null as number | null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
allDevices(): DisplayDevice[] {
|
||||
const list = Array.from(this.devicesMap.values())
|
||||
// Sort: Online first, then by ID
|
||||
return list.sort((a, b) => {
|
||||
if (a.isOnline && !b.isOnline) return -1
|
||||
if (!a.isOnline && b.isOnline) return 1
|
||||
return a.numericId - b.numericId
|
||||
})
|
||||
},
|
||||
onlineCount(): number {
|
||||
let count = 0
|
||||
const list = this.allDevices
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].isOnline) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.loadDbDevices()
|
||||
this.startMonitoring()
|
||||
this.timer = setInterval(() => {
|
||||
this.now = Date.now()
|
||||
this.checkOnlineStatus()
|
||||
}, 1000)
|
||||
},
|
||||
onUnload() {
|
||||
this.stopMonitoring()
|
||||
if (this.timer != null) {
|
||||
clearInterval(this.timer!)
|
||||
this.timer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadDbDevices() {
|
||||
try {
|
||||
const res = await supa.from('ak_devices').select('*').execute()
|
||||
if (res.error == null && res.data != null) {
|
||||
const list = res.data as UTSArray<UTSJSONObject>
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i]
|
||||
const id = item.getString('id') ?? ''
|
||||
const name = item.getString('device_name') ?? 'Unknown'
|
||||
const mac = item.getString('device_mac') ?? ''
|
||||
|
||||
// 兼容 watch_id 可能是数字或字符串的情况
|
||||
let watchId = item.getNumber('watch_id') ?? 0
|
||||
if (watchId == 0) {
|
||||
const wStr = item.getString('watch_id')
|
||||
if (wStr != null) {
|
||||
watchId = parseInt(wStr)
|
||||
if (isNaN(watchId)) watchId = 0
|
||||
}
|
||||
}
|
||||
|
||||
let key = ''
|
||||
if (watchId > 0) {
|
||||
key = `nid:${watchId}`
|
||||
} else if (mac != '') {
|
||||
key = mac
|
||||
} else {
|
||||
key = id
|
||||
}
|
||||
|
||||
const existing = this.devicesMap.get(key)
|
||||
if (existing != null) {
|
||||
// 更新已存在的设备信息(可能是实时数据先创建的)
|
||||
existing.dbId = id
|
||||
existing.mac = mac
|
||||
existing.numericId = watchId
|
||||
// 优先显示DB中的名称
|
||||
if (name != 'Unknown' && name != '') {
|
||||
existing.name = name
|
||||
}
|
||||
} else {
|
||||
const dev: DisplayDevice = {
|
||||
uniqueKey: key,
|
||||
dbId: id,
|
||||
numericId: watchId,
|
||||
name: name,
|
||||
mac: mac,
|
||||
isOnline: false,
|
||||
lastUpdateTime: 0,
|
||||
data: null
|
||||
}
|
||||
this.devicesMap.set(key, dev)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load devices', e)
|
||||
}
|
||||
},
|
||||
async startMonitoring() {
|
||||
this.subscription = await DeviceRealtimeService.subscribeAllDevices({
|
||||
onData: (data: DeviceBatchItem) => {
|
||||
this.handleDeviceData(data)
|
||||
},
|
||||
onError: (err: any) => {
|
||||
console.error('Device monitoring error', err)
|
||||
}
|
||||
})
|
||||
},
|
||||
stopMonitoring() {
|
||||
if (this.subscription != null) {
|
||||
this.subscription!.dispose()
|
||||
this.subscription = null
|
||||
}
|
||||
},
|
||||
handleDeviceData(data: DeviceBatchItem) {
|
||||
// 必须有RSSI信息才视为有效心跳
|
||||
if (data.rssi == 0) return
|
||||
|
||||
// 使用watch_id (data.id) 作为唯一标识
|
||||
const watchId = data.id
|
||||
const key = `nid:${watchId}`
|
||||
|
||||
// 优先使用数据中的接收时间,否则使用当前时间
|
||||
const updateTime = data.recvtime > 0 ? data.recvtime : Date.now()
|
||||
|
||||
let device = this.devicesMap.get(key)
|
||||
|
||||
// 过滤无效的 studentName
|
||||
const validStudentName = (data.studentName != '' && data.studentName != 'Unknown') ? data.studentName : ''
|
||||
|
||||
if (device == null) {
|
||||
device = {
|
||||
uniqueKey: key,
|
||||
dbId: null,
|
||||
numericId: watchId,
|
||||
name: validStudentName != '' ? validStudentName : `Device ${watchId}`,
|
||||
mac: '',
|
||||
isOnline: true,
|
||||
lastUpdateTime: updateTime,
|
||||
data: data
|
||||
}
|
||||
this.devicesMap.set(key, device)
|
||||
} else {
|
||||
device.isOnline = true
|
||||
device.lastUpdateTime = updateTime
|
||||
device.data = data
|
||||
// 只有当 studentName 有效时才更新名称,避免覆盖 DB 中的名称
|
||||
if (validStudentName != '') {
|
||||
device.name = validStudentName
|
||||
}
|
||||
}
|
||||
},
|
||||
checkOnlineStatus() {
|
||||
this.devicesMap.forEach((device) => {
|
||||
if (device.lastUpdateTime > 0) {
|
||||
// 180秒内视为在线
|
||||
device.isOnline = (this.now - device.lastUpdateTime) < 180000
|
||||
}
|
||||
})
|
||||
},
|
||||
formatTime(ts: number): string {
|
||||
if (ts == 0) return '-'
|
||||
return new Date(ts).toLocaleTimeString()
|
||||
},
|
||||
switchPage() {
|
||||
uni.redirectTo({
|
||||
url: '/pages/test/ap_monitor'
|
||||
})
|
||||
},
|
||||
openDeviceTest(device: DisplayDevice) {
|
||||
if (device.numericId > 0) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/test/device_monitor?id=${device.numericId}`
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '该设备无数字ID,无法测试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
margin: 0 5px;
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.nav-tab.active {
|
||||
background-color: #007aff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 15px 20px;
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.grid-layout {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.card-online {
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.card-offline {
|
||||
border-color: #e0e0e0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.badge-online {
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
|
||||
.badge-offline {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.badge-text {
|
||||
font-size: 10px;
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.badge-offline .badge-text {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: #d32f2f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.time-value {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
187
pages/test/popup-date-picker-portal-test.uvue
Normal file
187
pages/test/popup-date-picker-portal-test.uvue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<view class="test-container">
|
||||
<view class="header">
|
||||
<text class="title">Portal日期选择器测试</text>
|
||||
</view>
|
||||
|
||||
<view class="content">
|
||||
<view class="nav-section">
|
||||
<button @click="goToComparison" class="nav-btn">
|
||||
<text>查看Portal vs 原始方式对比</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="test-section">
|
||||
<text class="section-title">Portal版本日期选择器</text>
|
||||
<popup-date-picker-portal
|
||||
v-model:value="testDate"
|
||||
placeholder="选择测试日期"
|
||||
title="测试日期选择"
|
||||
theme="dark"
|
||||
@change="onDateChange"
|
||||
/>
|
||||
<text v-if="testDate" class="selected-date">选中日期: {{ testDate }}</text>
|
||||
</view>
|
||||
|
||||
<view class="test-section">
|
||||
<text class="section-title">原版日期选择器(z-index问题)</text>
|
||||
<popup-date-picker
|
||||
v-model:value="testDate2"
|
||||
placeholder="选择测试日期"
|
||||
title="测试日期选择"
|
||||
theme="light"
|
||||
@change="onDateChange2"
|
||||
/>
|
||||
<text v-if="testDate2" class="selected-date">选中日期: {{ testDate2 }}</text>
|
||||
</view>
|
||||
|
||||
<view class="high-z-element">
|
||||
<text class="high-z-text">高z-index元素 (z-index: 50000)</text>
|
||||
</view>
|
||||
|
||||
<view class="medium-z-element">
|
||||
<text class="medium-z-text">中等z-index元素 (z-index: 10000)</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import PopupDatePicker from '@/components/popup-date-picker/popup-date-picker.uvue'
|
||||
import PopupDatePickerPortal from '@/components/popup-date-picker/popup-date-picker-portal.uvue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PopupDatePicker,
|
||||
PopupDatePickerPortal
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
testDate: '',
|
||||
testDate2: ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onDateChange(date: string) {
|
||||
console.log('Portal版本日期选择:', date)
|
||||
},
|
||||
|
||||
onDateChange2(date: string) {
|
||||
console.log('原版日期选择:', date)
|
||||
},
|
||||
|
||||
goToComparison() {
|
||||
uni.navigateTo({
|
||||
url: '/pages/test/portal-vs-original-test'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style> .test-container {
|
||||
height: 100vh;
|
||||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 40rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 30rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
padding: 25rpx 40rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: none;
|
||||
border-radius: 15rpx;
|
||||
font-size: 28rpx;
|
||||
color: white;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.nav-btn:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
color: white;
|
||||
margin-bottom: 20rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.selected-date {
|
||||
margin-top: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.high-z-element {
|
||||
position: fixed;
|
||||
top: 200rpx;
|
||||
right: 50rpx;
|
||||
width: 300rpx;
|
||||
height: 100rpx;
|
||||
background: #FF6B6B;
|
||||
z-index: 50000;
|
||||
border-radius: 15rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10rpx 30rpx rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.high-z-text {
|
||||
color: white;
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.medium-z-element {
|
||||
position: fixed;
|
||||
top: 350rpx;
|
||||
right: 50rpx;
|
||||
width: 250rpx;
|
||||
height: 80rpx;
|
||||
background: #4ECDC4;
|
||||
z-index: 10000;
|
||||
border-radius: 15rpx;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10rpx 30rpx rgba(78, 205, 196, 0.3);
|
||||
}
|
||||
|
||||
.medium-z-text {
|
||||
color: white;
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
149
pages/test/popup-date-picker-test.uvue
Normal file
149
pages/test/popup-date-picker-test.uvue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<view class="test-container">
|
||||
<text class="title">弹出式日期选择器测试</text>
|
||||
|
||||
<view class="test-section">
|
||||
<text class="section-title">深色主题(适用于渐变背景)</text>
|
||||
<view class="dark-background">
|
||||
<popup-date-picker
|
||||
v-model:value="startDate"
|
||||
placeholder="选择开始日期"
|
||||
title="选择开始日期"
|
||||
theme="dark"
|
||||
@change="onStartDateChange"
|
||||
/>
|
||||
<popup-date-picker
|
||||
v-model:value="endDate"
|
||||
placeholder="选择结束日期"
|
||||
title="选择结束日期"
|
||||
theme="dark"
|
||||
@change="onEndDateChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="test-section">
|
||||
<text class="section-title">浅色主题(适用于白色背景)</text>
|
||||
<view class="light-background">
|
||||
<popup-date-picker
|
||||
v-model:value="testDate"
|
||||
placeholder="选择测试日期"
|
||||
title="选择测试日期"
|
||||
theme="light"
|
||||
@change="onTestDateChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="result-section">
|
||||
<text class="result-title">选择结果:</text>
|
||||
<text class="result-text">开始日期: {{ startDate || '未选择' }}</text>
|
||||
<text class="result-text">结束日期: {{ endDate || '未选择' }}</text>
|
||||
<text class="result-text">测试日期: {{ testDate || '未选择' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import PopupDatePicker from '@/components/popup-date-picker/popup-date-picker.uvue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PopupDatePicker
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
testDate: ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onStartDateChange(date: string) {
|
||||
console.log('开始日期选择:', date)
|
||||
uni.showToast({
|
||||
title: `开始日期: ${date}`,
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
onEndDateChange(date: string) {
|
||||
console.log('结束日期选择:', date)
|
||||
uni.showToast({
|
||||
title: `结束日期: ${date}`,
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
onTestDateChange(date: string) {
|
||||
console.log('测试日期选择:', date)
|
||||
uni.showToast({
|
||||
title: `测试日期: ${date}`,
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.test-container {
|
||||
padding: 40rpx;
|
||||
min-height: 100vh;
|
||||
background: #F5F5F5;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 400;
|
||||
color: #666;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.dark-background {
|
||||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||||
padding: 40rpx;
|
||||
border-radius: 20rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.light-background {
|
||||
background: #FFFFFF;
|
||||
padding: 40rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
background: #FFFFFF;
|
||||
padding: 40rpx;
|
||||
border-radius: 20rpx;
|
||||
border: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
</style>
|
||||
197
pages/test/popup-input-date-test.uvue
Normal file
197
pages/test/popup-input-date-test.uvue
Normal file
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<view class="input-date-test-container">
|
||||
<view class="header">
|
||||
<text class="title">弹出输入日期组件测试</text>
|
||||
<text class="subtitle">点击日期选择器体验弹出输入功能</text>
|
||||
</view>
|
||||
|
||||
<view class="test-content">
|
||||
<view class="test-section">
|
||||
<text class="section-title">基础版本</text>
|
||||
<popup-input-date
|
||||
v-model:value="basicDate"
|
||||
placeholder="选择日期"
|
||||
title="基础日期选择"
|
||||
theme="light"
|
||||
@change="onBasicDateChange"
|
||||
/>
|
||||
<text v-if="basicDate" class="result-text">选择结果: {{ basicDate }}</text>
|
||||
</view>
|
||||
|
||||
<view class="test-section">
|
||||
<text class="section-title">增强版本(推荐)</text>
|
||||
<popup-input-date-enhanced
|
||||
v-model:value="enhancedDate"
|
||||
placeholder="选择日期"
|
||||
title="增强日期选择"
|
||||
theme="dark"
|
||||
@change="onEnhancedDateChange"
|
||||
/>
|
||||
<text v-if="enhancedDate" class="result-text">选择结果: {{ enhancedDate }}</text>
|
||||
</view>
|
||||
|
||||
<view class="test-section">
|
||||
<text class="section-title">日期范围选择</text>
|
||||
<view class="date-range-demo">
|
||||
<popup-input-date-enhanced
|
||||
v-model:value="startDate"
|
||||
placeholder="开始日期"
|
||||
title="选择开始日期"
|
||||
theme="dark"
|
||||
@change="onRangeDateChange"
|
||||
/>
|
||||
<text class="range-separator">至</text>
|
||||
<popup-input-date-enhanced
|
||||
v-model:value="endDate"
|
||||
placeholder="结束日期"
|
||||
title="选择结束日期"
|
||||
theme="dark"
|
||||
@change="onRangeDateChange"
|
||||
/>
|
||||
</view>
|
||||
<text v-if="startDate && endDate" class="result-text">
|
||||
日期范围: {{ startDate }} 至 {{ endDate }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="feature-list">
|
||||
<text class="feature-title">功能特点:</text>
|
||||
<text class="feature-item">✅ 弹出输入框,无z-index问题</text>
|
||||
<text class="feature-item">✅ 支持手动输入和快捷选择</text>
|
||||
<text class="feature-item">✅ 自动日期格式验证</text>
|
||||
<text class="feature-item">✅ 支持v-model双向绑定</text>
|
||||
<text class="feature-item">✅ 深色/浅色主题支持</text>
|
||||
<text class="feature-item">✅ 今天/昨天/一周前快捷选择</text>
|
||||
<text class="feature-item">✅ 清空日期功能</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import PopupInputDate from '@/components/popup-date-picker/popup-input-date.uvue'
|
||||
import PopupInputDateEnhanced from '@/components/popup-date-picker/popup-input-date-enhanced.uvue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PopupInputDate,
|
||||
PopupInputDateEnhanced
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
basicDate: '',
|
||||
enhancedDate: '',
|
||||
startDate: '',
|
||||
endDate: ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onBasicDateChange(date: string) {
|
||||
console.log('基础版本日期选择:', date)
|
||||
},
|
||||
|
||||
onEnhancedDateChange(date: string) {
|
||||
console.log('增强版本日期选择:', date)
|
||||
},
|
||||
|
||||
onRangeDateChange() {
|
||||
if (this.startDate && this.endDate) {
|
||||
console.log('日期范围:', this.startDate, '至', this.endDate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style> .input-date-test-container {
|
||||
min-height: 100vh;
|
||||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.test-content {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx;
|
||||
backdrop-filter: blur(10px);
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
gap: 20rpx;
|
||||
padding: 30rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 400;
|
||||
color: white;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 15rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
margin-top: 15rpx;
|
||||
}
|
||||
|
||||
.date-range-demo {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 15rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.range-separator {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin: 0 10rpx;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 30rpx;
|
||||
border-radius: 15rpx;
|
||||
gap: 15rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 400;
|
||||
color: white;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
</style>
|
||||
143
pages/test/popup-overlay-test.uvue
Normal file
143
pages/test/popup-overlay-test.uvue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<view class="test-container">
|
||||
<view class="header">
|
||||
<text class="title">弹出层Z-Index测试</text>
|
||||
</view>
|
||||
|
||||
<view class="content">
|
||||
<view class="test-section">
|
||||
<text class="section-title">日期选择器测试</text>
|
||||
<popup-date-picker
|
||||
v-model:value="testDate"
|
||||
placeholder="选择测试日期"
|
||||
title="测试日期选择"
|
||||
theme="dark"
|
||||
@change="onDateChange"
|
||||
/>
|
||||
<text v-if="testDate" class="selected-date">选中日期: {{ testDate }}</text>
|
||||
</view>
|
||||
|
||||
<view class="high-z-element">
|
||||
<text class="high-z-text">高z-index元素 (z-index: 50000)</text>
|
||||
</view>
|
||||
|
||||
<view class="medium-z-element">
|
||||
<text class="medium-z-text">中等z-index元素 (z-index: 10000)</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import PopupDatePicker from '@/components/popup-date-picker/popup-date-picker.uvue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PopupDatePicker
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
testDate: ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onDateChange(date: string) {
|
||||
console.log('日期选择:', date)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped> .test-container {
|
||||
flex: 1;
|
||||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 40rpx 30rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: #FFFFFF;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
padding: 30rpx;
|
||||
background: #F8FAFC;
|
||||
border-radius: 30rpx 30rpx 0 0;
|
||||
margin-top: 20rpx;
|
||||
gap: 30rpx;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: #FFFFFF;
|
||||
padding: 30rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #1E293B;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.selected-date {
|
||||
font-size: 28rpx;
|
||||
color: #6366F1;
|
||||
padding: 15rpx;
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-radius: 15rpx;
|
||||
margin-top: 15rpx;
|
||||
}
|
||||
|
||||
.high-z-element {
|
||||
position: fixed;
|
||||
top: 200rpx;
|
||||
right: 30rpx;
|
||||
width: 200rpx;
|
||||
height: 100rpx;
|
||||
background: #EF4444;
|
||||
border-radius: 15rpx;
|
||||
z-index: 50000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.high-z-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.medium-z-element {
|
||||
position: fixed;
|
||||
top: 320rpx;
|
||||
right: 30rpx;
|
||||
width: 200rpx;
|
||||
height: 100rpx;
|
||||
background: #F59E0B;
|
||||
border-radius: 15rpx;
|
||||
z-index: 10000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.medium-z-text {
|
||||
color: #FFFFFF;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
220
pages/test/portal-vs-original-test.uvue
Normal file
220
pages/test/portal-vs-original-test.uvue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<view class="portal-test-container">
|
||||
<view class="header">
|
||||
<text class="title">Portal日期选择器测试</text>
|
||||
<text class="subtitle">点击下方按钮测试新的portal解决方案</text>
|
||||
</view>
|
||||
|
||||
<view class="test-content">
|
||||
<view class="test-item">
|
||||
<text class="test-label">Portal方式(推荐):</text>
|
||||
<popup-date-picker-portal
|
||||
v-model:value="portalDate"
|
||||
placeholder="选择日期(Portal)"
|
||||
title="Portal日期选择"
|
||||
theme="light"
|
||||
@change="onPortalDateChange"
|
||||
/>
|
||||
<text v-if="portalDate" class="result-text">选择结果: {{ portalDate }}</text>
|
||||
</view>
|
||||
|
||||
<view class="test-item">
|
||||
<text class="test-label">原有方式(z-index问题):</text>
|
||||
<popup-date-picker
|
||||
v-model:value="originalDate"
|
||||
placeholder="选择日期(原始)"
|
||||
title="原始日期选择"
|
||||
theme="light"
|
||||
@change="onOriginalDateChange"
|
||||
/>
|
||||
<text v-if="originalDate" class="result-text">选择结果: {{ originalDate }}</text>
|
||||
</view>
|
||||
|
||||
<view class="problem-demo">
|
||||
<text class="demo-title">z-index覆盖测试</text>
|
||||
<view class="high-z-box">
|
||||
<text class="high-z-text">高z-index元素 (50000)</text>
|
||||
</view>
|
||||
<view class="medium-z-box">
|
||||
<text class="medium-z-text">中等z-index元素 (10000)</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="instructions">
|
||||
<text class="instruction-title">测试说明:</text>
|
||||
<text class="instruction-text">1. Portal方式会跳转到新页面选择日期,确保不受z-index影响</text>
|
||||
<text class="instruction-text">2. 原始方式的弹窗可能被上方的高z-index元素遮挡</text>
|
||||
<text class="instruction-text">3. Portal方式提供更好的用户体验和更稳定的显示效果</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import PopupDatePicker from '@/components/popup-date-picker/popup-date-picker.uvue'
|
||||
import PopupDatePickerPortal from '@/components/popup-date-picker/popup-date-picker-portal.uvue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PopupDatePicker,
|
||||
PopupDatePickerPortal
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
portalDate: '',
|
||||
originalDate: ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onPortalDateChange(date: string) {
|
||||
console.log('Portal选择日期:', date)
|
||||
uni.showToast({
|
||||
title: `Portal选择: ${date}`,
|
||||
icon: 'success'
|
||||
})
|
||||
},
|
||||
|
||||
onOriginalDateChange(date: string) {
|
||||
console.log('原始选择日期:', date)
|
||||
uni.showToast({
|
||||
title: `原始选择: ${date}`,
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style> .portal-test-container {
|
||||
min-height: 100vh;
|
||||
background-image: linear-gradient(to bottom right, #667eea, #764ba2);
|
||||
padding: 40rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 60rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 48rpx;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.test-content {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20rpx;
|
||||
padding: 40rpx;
|
||||
backdrop-filter: blur(10px);
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.test-item {
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.test-label {
|
||||
font-size: 32rpx;
|
||||
font-weight: 400;
|
||||
color: white;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.result-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 15rpx 20rpx;
|
||||
border-radius: 10rpx;
|
||||
margin-top: 15rpx;
|
||||
}
|
||||
|
||||
.problem-demo {
|
||||
position: relative;
|
||||
margin: 40rpx 0;
|
||||
padding: 30rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 15rpx;
|
||||
}
|
||||
|
||||
.demo-title {
|
||||
font-size: 28rpx;
|
||||
color: white;
|
||||
margin-bottom: 20rpx;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.high-z-box {
|
||||
position: absolute;
|
||||
top: 80rpx;
|
||||
right: 20rpx;
|
||||
width: 200rpx;
|
||||
height: 80rpx;
|
||||
background: #FF6B6B;
|
||||
z-index: 50000;
|
||||
border-radius: 10rpx;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 10rpx 30rpx rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.high-z-text {
|
||||
color: white;
|
||||
font-size: 22rpx;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.medium-z-box {
|
||||
position: absolute;
|
||||
top: 180rpx;
|
||||
right: 40rpx;
|
||||
width: 160rpx;
|
||||
height: 60rpx;
|
||||
background: #4ECDC4;
|
||||
z-index: 10000;
|
||||
border-radius: 10rpx;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 8rpx 25rpx rgba(78, 205, 196, 0.3);
|
||||
}
|
||||
|
||||
.medium-z-text {
|
||||
color: white;
|
||||
font-size: 20rpx;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 30rpx;
|
||||
border-radius: 15rpx;
|
||||
gap: 15rpx;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.instruction-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 400;
|
||||
color: white;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.instruction-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
</style>
|
||||
9
pages/test/slottestpage.uvue
Normal file
9
pages/test/slottestpage.uvue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<slottest ref="udb" v-slot:default="{ data }">
|
||||
<!--
|
||||
<view>分页: 当前{{ (data as Array<UTSJSONObject>)}}</view> -->
|
||||
<view v-for="(item, idx) in (data as Array<UTSJSONObject>)" :key="idx">
|
||||
<view>{{ item.get("a") }} </view>
|
||||
</view>
|
||||
</slottest>
|
||||
</template>
|
||||
0
pages/test/sort-test.uvue
Normal file
0
pages/test/sort-test.uvue
Normal file
113
pages/test/supadbtest.uvue
Normal file
113
pages/test/supadbtest.uvue
Normal file
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<supadb ref="udb" :collection="collectionList" field="name,id" :where="where" page-data="replace" :orderby="orderby"
|
||||
getcount="exact" :page-size="pageSize" :page-current="pageCurrent"
|
||||
v-slot:default="{ data, pagination, hasmore ,loading, error }" loadtime="manual" :datafunc="addAkPrefix"
|
||||
@load="onqueryload">
|
||||
<view v-if="loading">加载<E58AA0>?..</view>
|
||||
<view v-else-if="error">{{ error }}</view>
|
||||
<view v-else>
|
||||
<view v-for="(item, idx) in (data as Array<UTSJSONObject>)" :key="idx">
|
||||
<!-- #ifdef APP-ANDROID -->
|
||||
<view>{{ item.get("name") }} - {{ item.get("id") }}</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-ANDROID -->
|
||||
<view>{{ item.name }} - {{ item.id }}</view>
|
||||
<!-- #endif -->
|
||||
|
||||
</view>
|
||||
<!-- 分页按钮 -->
|
||||
<view class="pagination-btns">
|
||||
<button @click="prevPage" >上一<E4B88A>?/button>
|
||||
<!-- Platform-specific pagination display for UTSJSONObject compatibility -->
|
||||
<!-- #ifdef APP-ANDROID || APP-IOS -->
|
||||
<text>第{{ (pagination as UTSJSONObject).getNumber("current") }}页</text>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-ANDROID || APP-IOS -->
|
||||
<text>第{{ (pagination as any)["current"] }}页</text>
|
||||
<!-- #endif -->
|
||||
<button @click="nextPage" :disabled="hasmore==false">下一<E4B88B>?/button>
|
||||
</view>
|
||||
</view>
|
||||
</supadb>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
type Showdata = {
|
||||
name : String,
|
||||
id : Number
|
||||
}
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
// supadb: null as SupadbComponentPublicInstance | null,
|
||||
showdata: [] as Array<UTSJSONObject>,
|
||||
collectionList: 'system_dept',
|
||||
where: {},
|
||||
orderby: '',
|
||||
totalcount: 0,
|
||||
pageSize: 20,
|
||||
pageCurrent: 1,
|
||||
|
||||
};
|
||||
},
|
||||
onReady() {
|
||||
// this.supadb = this.$refs["udb"] as SupadbComponentPublicInstance;
|
||||
// this.supadb?.loadData?.({ clear: false })
|
||||
},
|
||||
methods: { onqueryload(data : UTSJSONObject[]) {
|
||||
console.log('Data loaded:', data);
|
||||
this.showdata = data
|
||||
// const ttt:Showdata= data//{name:'aaa',id:1}
|
||||
// this.showdata = [ttt]//data as Array<Showdata>
|
||||
// // console.log(this.showdata[0])
|
||||
|
||||
// Removed map operation for UTS compatibility
|
||||
},
|
||||
onPageChanged(page : number) {
|
||||
this.pageCurrent = page;
|
||||
|
||||
// this.supadb?.loadData?.({ clear: false })
|
||||
}, addAkPrefix: function (items : UTSJSONObject[]) {
|
||||
// Replace map operation with for loop
|
||||
const result: UTSJSONObject[] = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
let newItem = new UTSJSONObject(item); // 复制原对象
|
||||
|
||||
// Platform-specific property access for UTSJSONObject compatibility
|
||||
let name: any = null
|
||||
|
||||
// #ifdef APP-ANDROID || APP-IOS
|
||||
// Native platform: use UTSJSONObject methods
|
||||
name = item.get("name");
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID || APP-IOS
|
||||
// Web platform: direct property access
|
||||
name = (item as any)["name"];
|
||||
// #endif
|
||||
|
||||
if (typeof name === "string") {
|
||||
newItem.set("name", "ak_" + name);
|
||||
}
|
||||
result.push(newItem)
|
||||
}
|
||||
return result
|
||||
},
|
||||
prevPage()
|
||||
{
|
||||
this.pageCurrent = this.pageCurrent -1;
|
||||
console.log(this.pageCurrent)
|
||||
},
|
||||
nextPage()
|
||||
{
|
||||
this.pageCurrent +=1;
|
||||
console.log(this.pageCurrent)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 添加您的样式 */
|
||||
</style>
|
||||
212
pages/test/suparealtimetest.uvue
Normal file
212
pages/test/suparealtimetest.uvue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<view class="test-container">
|
||||
<text class="title">Supabase Realtime 测试</text>
|
||||
<view class="section">
|
||||
<text>WS地址></text>
|
||||
<input v-model="wsUrl" placeholder="ws(s)://..." />
|
||||
</view>
|
||||
<view class="section">
|
||||
<text>频道></text>
|
||||
<input v-model="channel" placeholder="realtime:public:ak_info" />
|
||||
</view>
|
||||
<view class="section">
|
||||
<text>Token></text>
|
||||
<input v-model="token" placeholder="可选,JWT" />
|
||||
</view>
|
||||
<view class="section">
|
||||
<button @click="connect">连接</button>
|
||||
<button @click="close">断开</button>
|
||||
</view>
|
||||
<view class="section">
|
||||
<button @click="testSignIn">signIn</button>
|
||||
</view>
|
||||
<view class="section">
|
||||
<button @click="subscribeInsert">订阅INSERT</button>
|
||||
</view>
|
||||
<view class="section">
|
||||
<button @click="sendTestMessage">手动发送消息</button>
|
||||
</view>
|
||||
<view class="section">
|
||||
<text>消息></text>
|
||||
<scroll-view style="height: 300rpx; border: 1px solid #ccc; padding: 8rpx;" scroll-y>
|
||||
<view v-for="(msg, idx) in messages" :key="idx" style="font-size: 24rpx; color: #333; margin-bottom: 8rpx;">
|
||||
{{ msg }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import supa from '@/components/supadb/aksupainstance.uts';
|
||||
|
||||
import { AkSupaRealtime } from '@/components/supadb/aksuparealtime.uts';
|
||||
import { AkReq } from '@/uni_modules/ak-req/ak-req.uts';
|
||||
import { SUPA_URL, SUPA_KEY ,WS_URL } from '@/ak/config.uts';
|
||||
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
email: 'akoo@163.com',
|
||||
password: 'Hf2152111',
|
||||
wsUrl: WS_URL,
|
||||
channel: 'realtime:public:ps_push_msg_raw',
|
||||
token: '',
|
||||
messages: [] as string[],
|
||||
realtime: null as AkSupaRealtime | null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
async testSignIn() {
|
||||
try {
|
||||
const res = await supa.signIn(this.email, this.password);
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
connect() {
|
||||
|
||||
// 先安全关闭 realtime 实例
|
||||
const oldRealtime = this.realtime;
|
||||
if (oldRealtime != null) {
|
||||
oldRealtime.close({}); // 传递空对象,兼容 UTS 类型
|
||||
this.realtime = null;
|
||||
}
|
||||
let token = AkReq.getToken();
|
||||
console.log(token);
|
||||
if (token == null || token === "") {
|
||||
this.messages.push('未检测到 access_token,请先登录');
|
||||
return;
|
||||
}
|
||||
// wsUrl/channel 直接 String(this.wsUrl) 兼容 UTS
|
||||
const wsUrl: string = this.wsUrl as string;
|
||||
const channel: string = this.channel!!;
|
||||
const self = this;
|
||||
const newRealtime = new AkSupaRealtime({
|
||||
url: wsUrl,
|
||||
channel: channel,
|
||||
token: token,
|
||||
apikey: SUPA_KEY, // 如需 apikey 可补充
|
||||
onOpen(res) {
|
||||
console.log(res)
|
||||
self.messages.push('WebSocket 已连接');
|
||||
},
|
||||
onClose(res) {
|
||||
console.log(res)
|
||||
self.messages.push('WebSocket 已断开');
|
||||
},
|
||||
onError(err) {
|
||||
self.messages.push('WebSocket 错误: ' + JSON.stringify(err));
|
||||
},
|
||||
onMessage(data) {
|
||||
self.messages.push('收到消息: ' + JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
this.realtime = newRealtime;
|
||||
newRealtime.connect();
|
||||
this.messages.push('正在连接...');
|
||||
},
|
||||
close() {
|
||||
const oldRealtime = this.realtime;
|
||||
if (oldRealtime != null) {
|
||||
oldRealtime.close({}); // 传递空对象,兼容 UTS 类型
|
||||
this.realtime = null;
|
||||
this.messages.push('手动断开连接');
|
||||
}
|
||||
},
|
||||
subscribeInsert() {
|
||||
// 先安全关闭旧实例
|
||||
const oldRealtime = this.realtime;
|
||||
if (oldRealtime != null) {
|
||||
oldRealtime.close({});
|
||||
this.realtime = null;
|
||||
}
|
||||
let token = AkReq.getToken();
|
||||
if (!token) {
|
||||
this.messages.push('未检测到 access_token,请先登录');
|
||||
return;
|
||||
}
|
||||
const wsUrl: string = this.wsUrl as string;
|
||||
// 频道名可自定义为 'elder',事件类型和表名按需传递
|
||||
const newRealtime = new AkSupaRealtime({
|
||||
url: wsUrl,
|
||||
channel: 'elder',
|
||||
token: token,
|
||||
apikey: SUPA_KEY,
|
||||
onOpen: () => {
|
||||
this.messages.push('订阅已连接');
|
||||
// 订阅 Postgres INSERT 事件
|
||||
newRealtime.subscribePostgresChanges({
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
table: 'ps_push_msg_raw',
|
||||
onChange: (payload) => {
|
||||
this.messages.push('Change received! ' + JSON.stringify(payload));
|
||||
console.log('Change received!', payload);
|
||||
}
|
||||
});
|
||||
},
|
||||
onClose: () => {
|
||||
this.messages.push('订阅已断开');
|
||||
},
|
||||
onError: (err) => {
|
||||
this.messages.push('订阅错误: ' + JSON.stringify(err));
|
||||
},
|
||||
onMessage: (data) => {
|
||||
console.log(data)
|
||||
// 可选:收到其它消息
|
||||
}
|
||||
});
|
||||
this.realtime = newRealtime;
|
||||
newRealtime.connect();
|
||||
this.messages.push('正在订阅 INSERT...');
|
||||
},
|
||||
sendTestMessage() {
|
||||
if (!this.realtime || !this.realtime.isOpen) {
|
||||
this.messages.push('WebSocket 未连接,无法发送');
|
||||
return;
|
||||
}
|
||||
const testMsg = {
|
||||
event: 'phx_test',
|
||||
payload: { msg: 'Hello from client', time: Date.now() },
|
||||
ref: Date.now().toString(),
|
||||
topic: this.channel
|
||||
};
|
||||
this.realtime.send({
|
||||
data: testMsg,
|
||||
success: (res) => {
|
||||
this.messages.push('消息发送成功: ' + JSON.stringify(res));
|
||||
},
|
||||
fail: (err) => {
|
||||
this.messages.push('消息发送失败: ' + JSON.stringify(err));
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.test-container {
|
||||
padding: 32rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
input {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
button {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
</style>
|
||||
188
pages/test/supatest.uvue
Normal file
188
pages/test/supatest.uvue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<view class="test-container">
|
||||
<text class="title">AkSupa 功能测试</text>
|
||||
<view class="section">
|
||||
<text>邮箱></text>
|
||||
<input v-model="email" placeholder="邮箱" />
|
||||
</view>
|
||||
<view class="section">
|
||||
<text>密码></text>
|
||||
<input v-model="password" placeholder="密码" />
|
||||
</view>
|
||||
<view class="section">
|
||||
<button @click="testSignIn">signIn</button>
|
||||
<button @click="testSignUp">signUp</button>
|
||||
</view>
|
||||
<view class="section">
|
||||
<text>表名></text>
|
||||
<input v-model="table" placeholder="表名" />
|
||||
</view>
|
||||
<view class="section">
|
||||
<button @click="testSelect">select</button>
|
||||
<button @click="testInsert">insert</button>
|
||||
<button @click="testUpdate">update</button>
|
||||
<button @click="testDelete">delete</button>
|
||||
</view>
|
||||
<view class="section">
|
||||
<text>结果></text>
|
||||
<text>{{ result }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { AkSupa, AkSupaSelectOptions } from '@/components/supadb/aksupa.uts';
|
||||
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts';
|
||||
import supa from '@/components/supadb/aksupainstance.uts'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
email: 'akoo@163.com',
|
||||
password: 'Hf2152111',
|
||||
table: 'member_user',
|
||||
result: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getSupa() {
|
||||
return new AkSupa(SUPA_URL, SUPA_KEY);
|
||||
},
|
||||
async testSignIn() {
|
||||
try {
|
||||
const res = await this.getSupa().signIn(this.email, this.password);
|
||||
this.result = JSON.stringify(res, null, 2);
|
||||
} catch (e) {
|
||||
this.result = 'signIn error: ' + JSON.stringify(e);
|
||||
}
|
||||
},
|
||||
async testSignUp() {
|
||||
try {
|
||||
const res = await this.getSupa().signUp(this.email, this.password);
|
||||
this.result = JSON.stringify(res, null, 2);
|
||||
} catch (e) {
|
||||
this.result = 'signUp error: ' + JSON.stringify(e);
|
||||
}
|
||||
},
|
||||
async testSelect() {
|
||||
try {
|
||||
// 测试多种 filter 场景
|
||||
// 1. id < 800
|
||||
const filter1 = { id: { lt: 800 } } as UTSJSONObject;
|
||||
// 2. name ilike '%foo%'
|
||||
// const filter2 = { name: { ilike: '%foo%' } } as UTSJSONObject;
|
||||
// 3. status = 'active' <20>?age >= 18 <20>?age <= 30
|
||||
const filter3 = { status: 'active', age: { gte: 18, lte: 30 } } as UTSJSONObject;
|
||||
// 4. id in [1,2,3]
|
||||
const filter4 = { id: { in: [1,2,3] } } as UTSJSONObject;
|
||||
// 5. is null
|
||||
const filter5 = { deleted_at: { is: null } } as UTSJSONObject;
|
||||
// 6. not equal
|
||||
const filter6 = { status: { neq: 'inactive' } } as UTSJSONObject;
|
||||
// 7. 组合
|
||||
const filter7 = { id: { gte: 100, lte: 200 }, status: 'active' } as UTSJSONObject;
|
||||
// 你可以切换不<E68DA2>?filter 测试
|
||||
const filter = filter1;
|
||||
const options: AkSupaSelectOptions = { limit: 10, order: '', columns: '*' };
|
||||
// const res = await this.getSupa().select(this.table, filter, options);
|
||||
const res = supa.from('system_dept').select('*',{count:'exact'}).or("name.like.%202%,email.like.%202%").limit(10).execute();
|
||||
console.log(supa)
|
||||
// this.result = JSON.stringify(res, null, 2);
|
||||
} catch (e) {
|
||||
this.result = 'select error: ' + JSON.stringify(e);
|
||||
}
|
||||
},
|
||||
async testInsert() {
|
||||
try {
|
||||
// 插入一条测试数<E8AF95>?
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const row = {
|
||||
ztid: 1,
|
||||
cid: 2,
|
||||
classid: 3,
|
||||
id: 10001,
|
||||
newstime: now,
|
||||
mid: 1,
|
||||
isgood: 0,
|
||||
changetime: new Date().toISOString(),
|
||||
onclick: 0,
|
||||
scnum: 0,
|
||||
unzannum: 0,
|
||||
zannum: 0,
|
||||
plnum: 0,
|
||||
totaldown: 0,
|
||||
newspath: 'testpath',
|
||||
filename: 'testfile',
|
||||
userid: 123,
|
||||
username: 'testuser',
|
||||
firsttitle: 0,
|
||||
ispic: false,
|
||||
istop: false,
|
||||
isqf: false,
|
||||
ismember: false,
|
||||
isurl: false,
|
||||
truetime: now,
|
||||
lastdotime: now,
|
||||
havehtml: false,
|
||||
groupid: 0,
|
||||
userfen: 0,
|
||||
titlefont: '',
|
||||
titleurl: ''
|
||||
} as UTSJSONObject;
|
||||
const res = await this.getSupa().insert('ak_info', row);
|
||||
this.result = 'insert ok: ' + JSON.stringify(res, null, 2);
|
||||
} catch (e) {
|
||||
this.result = 'insert error: ' + JSON.stringify(e);
|
||||
}
|
||||
},
|
||||
async testUpdate() {
|
||||
try {
|
||||
// 更新 ak_info 表,username = 'testuser' 的记录,修改 onclick 字段
|
||||
const filter = { username: 'testuser' } as UTSJSONObject;
|
||||
const values = { onclick: 999, zannum: 123, titleurl: 'updated-url', isgood: 1 } as UTSJSONObject;
|
||||
const res = await this.getSupa().update('ak_info', filter, values);
|
||||
this.result = 'update ok: ' + JSON.stringify(res, null, 2);
|
||||
} catch (e) {
|
||||
this.result = 'update error: ' + JSON.stringify(e);
|
||||
}
|
||||
},
|
||||
async testDelete() {
|
||||
try {
|
||||
// 删除 username = 'testuser' 的记<E79A84>?
|
||||
const filter = { username: 'testuser' } as UTSJSONObject;
|
||||
const res = await this.getSupa().delete('ak_info', filter);
|
||||
this.result = 'delete ok: ' + JSON.stringify(res, null, 2);
|
||||
} catch (e) {
|
||||
this.result = 'delete error: ' + JSON.stringify(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.test-container {
|
||||
padding: 32rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
input {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
button {
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.result {
|
||||
word-break: break-all;
|
||||
color: #2196f3;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user