Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

315
pages/test/ap_monitor.uvue Normal file
View 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>

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

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

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

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

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

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

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

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

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

View File

113
pages/test/supadbtest.uvue Normal file
View 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>

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