Initial commit of akmon project
This commit is contained in:
533
components/supadb/RegionSelector.uvue
Normal file
533
components/supadb/RegionSelector.uvue
Normal file
@@ -0,0 +1,533 @@
|
||||
<template>
|
||||
<view class="region-selector">
|
||||
<view class="region-filter">
|
||||
<view class="level-tabs">
|
||||
<view
|
||||
v-for="(level, index) in availableLevels"
|
||||
:key="level.value"
|
||||
:class="['level-tab', { active: currentLevelIndex === index }]"
|
||||
@click="selectLevel(level.value, index)"
|
||||
>
|
||||
<text>{{ level.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="selected-path" v-if="showPath && selectedPath.length > 0">
|
||||
<view
|
||||
v-for="(region, index) in selectedPath"
|
||||
:key="region.id"
|
||||
class="path-item"
|
||||
>
|
||||
<text
|
||||
class="path-text"
|
||||
@click="navigateToPathItem(index)"
|
||||
>{{ region.name }}</text>
|
||||
<text class="path-separator" v-if="index < selectedPath.length - 1"> / </text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="region-list" v-if="regions.length > 0">
|
||||
<view class="region-items">
|
||||
<view
|
||||
v-for="region in regions"
|
||||
:key="region.id"
|
||||
class="region-item"
|
||||
@click="selectRegion(region)"
|
||||
>
|
||||
<view class="region-info">
|
||||
<text class="region-name">{{ region.name }}</text>
|
||||
<view class="region-meta" v-if="showStats">
|
||||
<text class="region-count">{{ region.children_count || 0 }} 个下级区域</text>
|
||||
<text class="region-count">{{ region.school_count || 0 }} 所学校</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="region-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty-state" v-else-if="!loading">
|
||||
<text>当前没有{{ currentLevelLabel }}级区域数据</text>
|
||||
<button
|
||||
v-if="canCreate"
|
||||
class="add-btn"
|
||||
@click="$emit('create', { parentId: currentParentId, level: currentLevel })"
|
||||
>
|
||||
添加{{ currentLevelLabel }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="loading" v-if="loading">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { SupaDB } from './supadb.uvue'
|
||||
|
||||
// 定义区域数据接口
|
||||
interface Region {
|
||||
id: string
|
||||
name: string
|
||||
level: number
|
||||
parent_id?: string
|
||||
children_count?: number
|
||||
school_count?: number
|
||||
}
|
||||
|
||||
// 定义级别选项接口
|
||||
interface LevelOption {
|
||||
value: number
|
||||
label: string
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'RegionSelector',
|
||||
props: {
|
||||
// 初始选中的区域ID
|
||||
initialRegionId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否显示路径导航
|
||||
showPath: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示统计数据
|
||||
showStats: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否可以创建新区域
|
||||
canCreate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 可用的区域级别,如果为空则使用所有级别
|
||||
allowedLevels: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
setup(props: any, { emit }: any) {
|
||||
const db = new SupaDB()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const regions = ref<Region[]>([])
|
||||
const selectedPath = ref<Region[]>([])
|
||||
const currentLevel = ref(1) // 默认省级
|
||||
const currentLevelIndex = ref(0)
|
||||
const currentParentId = ref('')
|
||||
|
||||
// 级别常量
|
||||
const REGION_LEVELS: LevelOption[] = [
|
||||
{ value: 1, label: '省/直辖市' },
|
||||
{ value: 2, label: '市/区' },
|
||||
{ value: 3, label: '县/区' },
|
||||
{ value: 4, label: '乡镇/街道' }
|
||||
]
|
||||
// 计算属性:可用的级别
|
||||
const availableLevels = computed(() => {
|
||||
if (props.allowedLevels && props.allowedLevels.length > 0) {
|
||||
// Replace filter with for loop for UTS compatibility
|
||||
let filteredLevels = []
|
||||
for (let i = 0; i < REGION_LEVELS.length; i++) {
|
||||
const level = REGION_LEVELS[i]
|
||||
if (props.allowedLevels.includes(level.value)) {
|
||||
filteredLevels.push(level)
|
||||
}
|
||||
}
|
||||
return filteredLevels
|
||||
}
|
||||
return REGION_LEVELS
|
||||
})
|
||||
|
||||
// 当前级别标签
|
||||
const currentLevelLabel = computed(() => {
|
||||
const level = REGION_LEVELS.find(l => l.value === currentLevel.value)
|
||||
return level ? level.label : ''
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
// 如果有初始区域ID,则加载该区域及其路径
|
||||
if (props.initialRegionId) {
|
||||
await loadRegionAndPath(props.initialRegionId)
|
||||
} else {
|
||||
// 否则加载顶级区域
|
||||
await loadRegions()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听初始区域ID变化
|
||||
watch(() => props.initialRegionId, async (newVal) => {
|
||||
if (newVal) {
|
||||
await loadRegionAndPath(newVal)
|
||||
}
|
||||
})
|
||||
|
||||
// 加载区域数据
|
||||
const loadRegions = async (parentId?: string) => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
let query = db.from('ak_regions')
|
||||
.select('*, children:ak_regions!parent_id(count), schools:ak_schools(count)')
|
||||
.eq('level', currentLevel.value)
|
||||
.order('name')
|
||||
|
||||
if (parentId) {
|
||||
query = query.eq('parent_id', parentId)
|
||||
} else if (currentLevel.value !== 1) {
|
||||
// 非顶级区域但无父ID,显示空数据
|
||||
regions.value = []
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const { data, error } = await query
|
||||
|
||||
if (error) {
|
||||
console.error('加载区域数据失败:', error)
|
||||
regions.value = []
|
||||
return
|
||||
}
|
||||
if (data) {
|
||||
// Replace map with for loop for UTS compatibility
|
||||
let mappedRegions : Region[] = []
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const item = data[i]
|
||||
mappedRegions.push({
|
||||
...item,
|
||||
children_count: (item.children && item.children.length) ? item.children[0].count : 0,
|
||||
school_count: (item.schools && item.schools.length) ? item.schools[0].count : 0
|
||||
} as Region)
|
||||
}
|
||||
regions.value = mappedRegions
|
||||
} else {
|
||||
regions.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载区域数据异常:', e)
|
||||
regions.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载区域及其路径
|
||||
const loadRegionAndPath = async (regionId: string) => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 获取区域详情
|
||||
const { data, error } = await db.from('ak_regions')
|
||||
.select('*')
|
||||
.eq('id', regionId)
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
console.error('获取区域详情失败:', error)
|
||||
return
|
||||
}
|
||||
|
||||
if (!data) return
|
||||
|
||||
const region = data as Region
|
||||
|
||||
// 设置当前级别
|
||||
currentLevel.value = region.level
|
||||
const levelIndex = availableLevels.value.findIndex(l => l.value === region.level)
|
||||
if (levelIndex >= 0) {
|
||||
currentLevelIndex.value = levelIndex
|
||||
}
|
||||
|
||||
// 获取路径
|
||||
await loadRegionPath(region)
|
||||
|
||||
// 加载同级区域
|
||||
if (region.parent_id) {
|
||||
currentParentId.value = region.parent_id
|
||||
await loadRegions(region.parent_id)
|
||||
} else {
|
||||
currentParentId.value = ''
|
||||
await loadRegions()
|
||||
}
|
||||
|
||||
// 触发选择事件
|
||||
emit('select', region)
|
||||
} catch (e) {
|
||||
console.error('加载区域及路径异常:', e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载区域路径
|
||||
const loadRegionPath = async (region: Region) => {
|
||||
try {
|
||||
// 先将当前区域添加到路径
|
||||
selectedPath.value = [region]
|
||||
|
||||
let currentParent = region.parent_id
|
||||
|
||||
// 循环获取所有父区域
|
||||
while (currentParent) {
|
||||
const { data, error } = await db.from('ak_regions')
|
||||
.select('*')
|
||||
.eq('id', currentParent)
|
||||
.single()
|
||||
|
||||
if (error || !data) break
|
||||
|
||||
const parentRegion = data as Region
|
||||
// 将父区域添加到路径前面
|
||||
selectedPath.value.unshift(parentRegion)
|
||||
|
||||
// 继续向上级查找
|
||||
currentParent = parentRegion.parent_id
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载区域路径异常:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择区域级别
|
||||
const selectLevel = async (level: number, index: number) => {
|
||||
currentLevel.value = level
|
||||
currentLevelIndex.value = index
|
||||
|
||||
// 根据当前路径确定父级ID
|
||||
if (selectedPath.value.length > 0) {
|
||||
// 找到合适的父级
|
||||
const parent = selectedPath.value.find(r => r.level === level - 1)
|
||||
|
||||
if (parent) {
|
||||
// 找到合适的父级,更新路径
|
||||
const pathIndex = selectedPath.value.indexOf(parent)
|
||||
selectedPath.value = selectedPath.value.slice(0, pathIndex + 1)
|
||||
currentParentId.value = parent.id
|
||||
} else {
|
||||
// 未找到合适的父级,重置路径
|
||||
selectedPath.value = []
|
||||
currentParentId.value = ''
|
||||
}
|
||||
} else {
|
||||
currentParentId.value = ''
|
||||
}
|
||||
|
||||
await loadRegions(currentParentId.value || undefined)
|
||||
}
|
||||
|
||||
// 选择区域
|
||||
const selectRegion = async (region: Region) => {
|
||||
// 如果已在路径中,则不重复添加
|
||||
if (selectedPath.value.some(r => r.id === region.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 更新路径
|
||||
if (region.level > 1 && selectedPath.value.length === 0) {
|
||||
// 如果是选择非顶级区域且路径为空,需要加载完整路径
|
||||
await loadRegionAndPath(region.id)
|
||||
} else {
|
||||
// 否则直接添加到路径末尾
|
||||
selectedPath.value.push(region)
|
||||
|
||||
// 如果有下级区域,自动切换到下级
|
||||
if (region.children_count && region.children_count > 0) {
|
||||
const nextLevel = region.level + 1
|
||||
const nextLevelOption = availableLevels.value.find(l => l.value === nextLevel)
|
||||
|
||||
if (nextLevelOption) {
|
||||
const nextLevelIndex = availableLevels.value.indexOf(nextLevelOption)
|
||||
currentLevel.value = nextLevel
|
||||
currentLevelIndex.value = nextLevelIndex
|
||||
currentParentId.value = region.id
|
||||
await loadRegions(region.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发选择事件
|
||||
emit('select', region)
|
||||
}
|
||||
|
||||
// 导航到路径项
|
||||
const navigateToPathItem = async (index: number) => {
|
||||
if (index >= selectedPath.value.length) return
|
||||
|
||||
const pathItem = selectedPath.value[index]
|
||||
|
||||
// 更新路径
|
||||
selectedPath.value = selectedPath.value.slice(0, index + 1)
|
||||
|
||||
// 更新级别
|
||||
currentLevel.value = pathItem.level
|
||||
const levelIndex = availableLevels.value.findIndex(l => l.value === pathItem.level)
|
||||
if (levelIndex >= 0) {
|
||||
currentLevelIndex.value = levelIndex
|
||||
}
|
||||
|
||||
// 更新父ID
|
||||
if (index === 0) {
|
||||
currentParentId.value = ''
|
||||
} else {
|
||||
currentParentId.value = selectedPath.value[index - 1].id
|
||||
}
|
||||
|
||||
// 加载区域
|
||||
await loadRegions(currentParentId.value || undefined)
|
||||
|
||||
// 触发选择事件
|
||||
emit('select', pathItem)
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
regions,
|
||||
selectedPath,
|
||||
currentLevel,
|
||||
currentLevelIndex,
|
||||
currentParentId,
|
||||
availableLevels,
|
||||
currentLevelLabel,
|
||||
|
||||
// 方法
|
||||
selectLevel,
|
||||
selectRegion,
|
||||
navigateToPathItem
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.region-selector {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.region-filter {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.level-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.level-tab {
|
||||
padding: 6px 12px;
|
||||
margin-right: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: #f5f5f5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.level-tab.active {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.selected-path {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.path-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.path-text {
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.path-separator {
|
||||
color: #bbb;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.region-list {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.region-items {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.region-item {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.region-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.region-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.region-name {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.region-meta {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.region-count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.region-arrow {
|
||||
color: #bbb;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 30px 15px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
margin-top: 15px;
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.loading {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
155
components/supadb/SIMPLIFIED_API_GUIDE.md
Normal file
155
components/supadb/SIMPLIFIED_API_GUIDE.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# AkSupa 简化API指南
|
||||
|
||||
## 概述
|
||||
|
||||
AkSupa 现在采用了**简化的单一方法**设计:只提供 `executeAs<T>()` 方法进行类型安全的数据访问,移除了所有冗余的类型转换方法。
|
||||
|
||||
## 重要变化
|
||||
|
||||
### 🚫 已移除的方法
|
||||
- `selectAs<T>()`
|
||||
- `insertAs<T>()`
|
||||
- `updateAs<T>()`
|
||||
- `deleteAs<T>()`
|
||||
- `rpcAs<T>()`
|
||||
|
||||
### ✅ 统一的方法
|
||||
- `executeAs<T>()` - 唯一的类型转换方法
|
||||
|
||||
## 设计理念
|
||||
|
||||
### 简洁性原则
|
||||
- **一个方法解决所有问题**:所有查询操作最终都要调用 `execute()`,`executeAs<T>()` 是其类型安全版本
|
||||
- **减少API复杂性**:不需要记住多个不同的方法名
|
||||
- **保持一致性**:无论是查询、插入、更新还是删除,都使用相同的方法
|
||||
|
||||
### 链式友好
|
||||
```typescript
|
||||
// 所有操作都遵循相同的模式
|
||||
const result = await supa
|
||||
.from('table')
|
||||
.operation() // select(), insert(), update(), delete(), rpc()
|
||||
.conditions() // eq(), gt(), like(), etc.
|
||||
.executeAs<T>(); // 统一的类型转换方法
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 查询数据
|
||||
```typescript
|
||||
// 多条记录
|
||||
const users = await supa
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('status', 'active')
|
||||
.executeAs<User[]>();
|
||||
|
||||
// 单条记录
|
||||
const user = await supa
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('id', 1)
|
||||
.single()
|
||||
.executeAs<User>();
|
||||
```
|
||||
|
||||
### 插入数据
|
||||
```typescript
|
||||
const newUser = await supa
|
||||
.from('users')
|
||||
.insert({
|
||||
name: 'John',
|
||||
email: 'john@example.com'
|
||||
})
|
||||
.executeAs<User>();
|
||||
```
|
||||
|
||||
### 更新数据
|
||||
```typescript
|
||||
const updatedUser = await supa
|
||||
.from('users')
|
||||
.update({ name: 'Jane' })
|
||||
.eq('id', 1)
|
||||
.executeAs<User>();
|
||||
```
|
||||
|
||||
### 删除数据
|
||||
```typescript
|
||||
const deletedUser = await supa
|
||||
.from('users')
|
||||
.delete()
|
||||
.eq('id', 1)
|
||||
.executeAs<User>();
|
||||
```
|
||||
|
||||
### RPC调用
|
||||
```typescript
|
||||
const result = await supa
|
||||
.from('any_table')
|
||||
.rpc('my_function', { param1: 'value1' })
|
||||
.executeAs<ResultType>();
|
||||
```
|
||||
|
||||
## 平台兼容性
|
||||
|
||||
| 平台 | 类型转换机制 | 说明 |
|
||||
|------|-------------|------|
|
||||
| Android | `UTSJSONObject.parse<T>()` | 真正的类型转换 |
|
||||
| HarmonyOS | `UTSJSONObject.parse<T>()` | 真正的类型转换 |
|
||||
| Web/iOS | `as T` | 类型断言 |
|
||||
|
||||
## 从旧版本迁移
|
||||
|
||||
### 旧代码
|
||||
```typescript
|
||||
// 旧方式 - 多个方法
|
||||
const users = await supa.selectAs<User[]>('users', null, { limit: 10 });
|
||||
const newUser = await supa.insertAs<User>('users', userData);
|
||||
const updated = await supa.updateAs<User>('users', filter, updateData);
|
||||
```
|
||||
|
||||
### 新代码
|
||||
```typescript
|
||||
// 新方式 - 统一方法
|
||||
const users = await supa
|
||||
.from('users')
|
||||
.select('*')
|
||||
.limit(10)
|
||||
.executeAs<User[]>();
|
||||
|
||||
const newUser = await supa
|
||||
.from('users')
|
||||
.insert(userData)
|
||||
.executeAs<User>();
|
||||
|
||||
const updated = await supa
|
||||
.from('users')
|
||||
.update(updateData)
|
||||
.eq('id', userId)
|
||||
.executeAs<User>();
|
||||
```
|
||||
|
||||
## 优势
|
||||
|
||||
1. **API简洁**:只需要记住一个方法
|
||||
2. **类型安全**:TypeScript 编译时检查
|
||||
3. **平台兼容**:Android/HarmonyOS 使用真正的类型转换
|
||||
4. **链式友好**:与现有的链式方法无缝集成
|
||||
5. **维护性强**:单一方法,减少维护成本
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **泛型类型**:确保传入正确的类型参数 `<T>`
|
||||
2. **错误处理**:检查 `result.error` 和 `result.data` 的有效性
|
||||
3. **性能考虑**:Android 平台的类型转换有轻微性能开销
|
||||
4. **调试模式**:开发时会有转换过程的控制台输出
|
||||
|
||||
## 总结
|
||||
|
||||
通过采用单一的 `executeAs<T>()` 方法,AkSupa 现在提供了:
|
||||
- 更简洁的API
|
||||
- 更好的类型安全
|
||||
- 更一致的使用体验
|
||||
- 更容易维护的代码
|
||||
|
||||
这个设计遵循了"简单就是美"的原则,让开发者能够更专注于业务逻辑而不是API的复杂性。
|
||||
194
components/supadb/TYPED_QUERIES_README.md
Normal file
194
components/supadb/TYPED_QUERIES_README.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# AkSupa executeAs<T>() 类型转换功能
|
||||
|
||||
## 概述
|
||||
|
||||
AkSupa 现在提供简洁的 `executeAs<T>()` 方法,支持链式请求的类型转换功能,可以直接返回指定的类型而不仅仅是 `UTSJSONObject`。
|
||||
|
||||
## 设计理念
|
||||
|
||||
遵循 **简洁性原则**,只提供一个 `executeAs<T>()` 方法来处理所有类型转换需求,因为:
|
||||
|
||||
1. **统一API**:所有操作最终都通过 `execute()` 处理,`executeAs<T>()` 是其类型安全版本
|
||||
2. **链式友好**:可以与所有现有的链式方法无缝组合
|
||||
3. **易于理解**:只需记住一个方法,降低学习成本
|
||||
4. **功能完整**:覆盖查询、插入、更新、删除、RPC 等所有操作
|
||||
|
||||
## 平台兼容性
|
||||
|
||||
| 平台 | 支持方式 | 说明 |
|
||||
|------|----------|------|
|
||||
| Android (uni-app x 3.90+) | `UTSJSONObject.parse()` | 真正的类型转换 |
|
||||
| Web | `as T` | 类型断言,编译时类型提示 |
|
||||
| iOS | `as T` | 类型断言,编译时类型提示 |
|
||||
| HarmonyOS (4.61+) | `UTSJSONObject.parse()` | 真正的类型转换 |
|
||||
|
||||
## 方法签名
|
||||
|
||||
```typescript
|
||||
async executeAs<T>() : Promise<AkReqResponse<T>>
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 定义数据类型
|
||||
|
||||
```typescript
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
avatar_url?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 查询操作
|
||||
|
||||
```typescript
|
||||
// 查询多条记录
|
||||
const usersResult = await supa
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('status', 'active')
|
||||
.limit(10)
|
||||
.executeAs<User[]>();
|
||||
|
||||
// 查询单条记录
|
||||
const userResult = await supa
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('id', 1)
|
||||
.single()
|
||||
.executeAs<User>();
|
||||
|
||||
// 复杂查询
|
||||
const complexQuery = await supa
|
||||
.from('posts')
|
||||
.select('*, users!posts_user_id_fkey(*)')
|
||||
.eq('status', 'published')
|
||||
.gt('created_at', '2024-01-01')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(20)
|
||||
.executeAs<Post[]>();
|
||||
```
|
||||
|
||||
### 3. 插入操作
|
||||
|
||||
```typescript
|
||||
const newUser = {
|
||||
name: '新用户',
|
||||
email: 'newuser@example.com'
|
||||
} as UTSJSONObject;
|
||||
|
||||
const insertResult = await supa
|
||||
.from('users')
|
||||
.insert(newUser)
|
||||
.executeAs<User[]>();
|
||||
```
|
||||
|
||||
### 4. 更新操作
|
||||
|
||||
```typescript
|
||||
const updateResult = await supa
|
||||
.from('users')
|
||||
.update({ name: '更新的名称' } as UTSJSONObject)
|
||||
.eq('id', 1)
|
||||
.executeAs<User[]>();
|
||||
```
|
||||
|
||||
### 5. 删除操作
|
||||
|
||||
```typescript
|
||||
const deleteResult = await supa
|
||||
.from('users')
|
||||
.delete()
|
||||
.eq('id', 1)
|
||||
.executeAs<User[]>();
|
||||
```
|
||||
|
||||
### 6. RPC 调用
|
||||
|
||||
```typescript
|
||||
const rpcResult = await supa
|
||||
.from('') // RPC 不需要 table
|
||||
.rpc('get_user_stats', { user_id: 1 } as UTSJSONObject)
|
||||
.executeAs<{ total_posts: number; total_likes: number }>();
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const result = await supa
|
||||
.from('users')
|
||||
.select('*')
|
||||
.executeAs<User[]>();
|
||||
|
||||
if (result.error) {
|
||||
console.error('查询失败:', result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用类型化的数据
|
||||
const users = result.data;
|
||||
if (users != null) {
|
||||
users.forEach(user => {
|
||||
// 现在有完整的类型提示
|
||||
console.log(user.name, user.email);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求异常:', error);
|
||||
}
|
||||
```
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
- 原有的 `execute()` 方法依然保持不变,返回 `UTSJSONObject`
|
||||
- 所有原有的链式方法都继续正常工作
|
||||
- `executeAs<T>()` 是附加功能,不影响现有代码
|
||||
|
||||
## 对比旧版本
|
||||
|
||||
### 旧版本(多方法)
|
||||
```typescript
|
||||
// 需要记住多个方法
|
||||
const users = await supa.selectAs<User[]>('users', filter, options);
|
||||
const inserted = await supa.insertAs<User>('users', data);
|
||||
const updated = await supa.updateAs<User[]>('users', filter, values);
|
||||
const deleted = await supa.deleteAs<User[]>('users', filter);
|
||||
const rpcResult = await supa.rpcAs<Stats>('func_name', params);
|
||||
```
|
||||
|
||||
### 新版本(统一方法)
|
||||
```typescript
|
||||
// 只需记住一个 executeAs<T>() 方法
|
||||
const users = await supa.from('users').select('*').executeAs<User[]>();
|
||||
const inserted = await supa.from('users').insert(data).executeAs<User>();
|
||||
const updated = await supa.from('users').update(values).eq('id', 1).executeAs<User[]>();
|
||||
const deleted = await supa.from('users').delete().eq('id', 1).executeAs<User[]>();
|
||||
const rpcResult = await supa.from('').rpc('func_name', params).executeAs<Stats>();
|
||||
```
|
||||
|
||||
## 优势
|
||||
|
||||
1. **API 简洁**:只需要记住一个 `executeAs<T>()` 方法
|
||||
2. **链式友好**:与所有现有方法完美组合
|
||||
3. **类型安全**:编译时检查 + 运行时转换(Android)
|
||||
4. **易于维护**:减少重复代码,统一处理逻辑
|
||||
5. **学习成本低**:从 `execute()` 到 `executeAs<T>()` 自然过渡
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. `UTSJSONObject.parse()` 仅在 Android 3.90+ 和 HarmonyOS 4.61+ 平台支持
|
||||
2. 其他平台使用类型断言,主要提供编译时类型检查
|
||||
3. 类型转换失败时会 fallback 到原始数据
|
||||
4. 建议在生产环境中进行充分的测试
|
||||
|
||||
## 技术实现
|
||||
|
||||
`executeAs<T>()` 内部:
|
||||
1. 调用原有的 `execute()` 方法获取结果
|
||||
2. 在 Android 平台使用 `UTSJSONObject.parse()` 进行类型转换
|
||||
3. 在其他平台使用类型断言提供类型提示
|
||||
4. 返回类型化的 `AkReqResponse<T>` 结果
|
||||
126
components/supadb/TYPE_CONVERSION_FIX_SUMMARY.md
Normal file
126
components/supadb/TYPE_CONVERSION_FIX_SUMMARY.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# AkSupa 类型转换错误修复总结
|
||||
|
||||
## 问题描述
|
||||
|
||||
在实现 `executeAs<T>()` 和相关类型转换方法时,遇到了以下 UTS 编译错误:
|
||||
|
||||
1. **泛型类型参数错误**:`Cannot use 'T' as reified type parameter`
|
||||
2. **类型推断错误**:`推断类型是T?(可为空的T),但预期的是Any`
|
||||
3. **方法参数错误**:`Too many arguments for public open fun select`
|
||||
4. **属性访问错误**:`Unresolved reference: data`
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 1. 移除泛型类型参数
|
||||
|
||||
**问题**:UTS 不支持 `UTSJSONObject.parse<T>()` 这种带泛型参数的调用方式。
|
||||
|
||||
**解决方案**:
|
||||
- 将 `item.parse<T>()` 改为 `item.parse()`
|
||||
- 将 `result.data.parse<T>()` 改为 `result.data.parse()`
|
||||
- 使用类型断言 `as T` 来提供类型提示
|
||||
|
||||
```typescript
|
||||
// 修复前
|
||||
convertedData = result.data.parse<T>();
|
||||
|
||||
// 修复后
|
||||
convertedData = result.data.parse();
|
||||
```
|
||||
|
||||
### 2. 简化方法签名
|
||||
|
||||
**问题**:`_convertResponse<T>()` 方法的泛型签名在 UTS 中无法正确处理。
|
||||
|
||||
**解决方案**:
|
||||
- 将 `_convertResponse<T>()` 改为 `_convertResponse()`
|
||||
- 返回类型改为 `AkReqResponse<any>`
|
||||
- 在调用处使用类型断言 `as AkReqResponse<T>`
|
||||
|
||||
```typescript
|
||||
// 修复前
|
||||
private _convertResponse<T>(result: AkReqResponse<any>): AkReqResponse<T>
|
||||
|
||||
// 修复后
|
||||
private _convertResponse(result: AkReqResponse<any>): AkReqResponse<any>
|
||||
```
|
||||
|
||||
### 3. 统一类型处理
|
||||
|
||||
**问题**:不同平台的类型处理逻辑不一致。
|
||||
|
||||
**解决方案**:
|
||||
- Android 平台:使用 `UTSJSONObject.parse()` 进行真正的类型转换
|
||||
- 其他平台:直接返回原始结果,通过类型断言提供类型提示
|
||||
|
||||
```typescript
|
||||
// Android 平台
|
||||
convertedData = result.data.parse();
|
||||
|
||||
// 其他平台
|
||||
return result; // 直接返回原始结果
|
||||
```
|
||||
|
||||
### 4. 空值处理优化
|
||||
|
||||
**问题**:`parse()` 方法可能返回 `null`,需要安全处理。
|
||||
|
||||
**解决方案**:
|
||||
- 增加 `null` 检查:`parsed != null ? parsed : item`
|
||||
- 保持原始数据作为 fallback
|
||||
|
||||
```typescript
|
||||
const parsed = item.parse();
|
||||
convertedArray.push(parsed != null ? parsed : item);
|
||||
```
|
||||
|
||||
## 修复的方法列表
|
||||
|
||||
### AkSupaQueryBuilder 类
|
||||
- ✅ `executeAs<T>()` - 链式查询的类型转换执行
|
||||
|
||||
### AkSupa 类
|
||||
- ✅ `selectAs<T>()` - 查询并类型转换
|
||||
- ✅ `insertAs<T>()` - 插入并类型转换
|
||||
- ✅ `updateAs<T>()` - 更新并类型转换
|
||||
- ✅ `deleteAs<T>()` - 删除并类型转换
|
||||
- ✅ `rpcAs<T>()` - RPC调用并类型转换
|
||||
- ✅ `_convertResponse()` - 私有类型转换方法
|
||||
|
||||
## 平台兼容性
|
||||
|
||||
| 平台 | 处理方式 | 效果 |
|
||||
|------|----------|------|
|
||||
| Android | `UTSJSONObject.parse()` | 真正的类型转换 |
|
||||
| iOS | 类型断言 `as T` | 编译时类型检查 |
|
||||
| Web | 类型断言 `as T` | 编译时类型检查 |
|
||||
| HarmonyOS | `UTSJSONObject.parse()` | 真正的类型转换 |
|
||||
|
||||
## 使用示例
|
||||
|
||||
```typescript
|
||||
// 现在可以正常使用了
|
||||
const users = await supa
|
||||
.from('users')
|
||||
.select('*')
|
||||
.executeAs<User[]>();
|
||||
|
||||
// 类型安全的访问
|
||||
users.data?.forEach(user => {
|
||||
console.log(user.name); // 有完整的类型提示
|
||||
});
|
||||
|
||||
// 直接方法调用
|
||||
const result = await supa.selectAs<User[]>('users');
|
||||
```
|
||||
|
||||
## 技术要点
|
||||
|
||||
1. **UTS 限制**:不支持泛型的 reified 类型参数
|
||||
2. **类型安全**:通过编译时类型断言提供类型提示
|
||||
3. **运行时转换**:在支持的平台上进行真正的类型转换
|
||||
4. **向后兼容**:原有的 `.execute()` 方法保持不变
|
||||
|
||||
## 总结
|
||||
|
||||
修复后的代码在保持类型安全的同时,完全兼容 UTS 的编译要求。在 Android 和 HarmonyOS 平台上提供真正的类型转换,在其他平台上提供编译时类型检查,为开发者提供了更好的开发体验。
|
||||
1027
components/supadb/aksupa - 副本.uts
Normal file
1027
components/supadb/aksupa - 副本.uts
Normal file
File diff suppressed because it is too large
Load Diff
1024
components/supadb/aksupa.uts
Normal file
1024
components/supadb/aksupa.uts
Normal file
File diff suppressed because it is too large
Load Diff
18
components/supadb/aksupainstance.uts
Normal file
18
components/supadb/aksupainstance.uts
Normal file
@@ -0,0 +1,18 @@
|
||||
import AkSupa from './aksupa.uts'
|
||||
import { SUPA_URL, SUPA_KEY } from '@/ak/config.uts'
|
||||
|
||||
const supa = new AkSupa(SUPA_URL, SUPA_KEY)
|
||||
|
||||
const supaReady: Promise<boolean> = (async () => {
|
||||
try {
|
||||
// await supa.signIn('akoo@163.com', 'Hf2152111')
|
||||
await supa.signIn('am@163.com', 'kookoo')
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('Supabase auto sign-in failed', err)
|
||||
return false
|
||||
}
|
||||
})()
|
||||
|
||||
export { supaReady }
|
||||
export default supa
|
||||
56
components/supadb/aksupareal.md
Normal file
56
components/supadb/aksupareal.md
Normal file
@@ -0,0 +1,56 @@
|
||||
GET wss://ak3.oulog.com/realtime/v1/websocket?apikey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q&vsn=1.0.0 HTTP/1.1
|
||||
Host: ak3.oulog.com
|
||||
Connection: Upgrade
|
||||
Pragma: no-cache
|
||||
Cache-Control: no-cache
|
||||
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1
|
||||
Upgrade: websocket
|
||||
Origin: http://localhost:5174
|
||||
Sec-WebSocket-Version: 13
|
||||
Accept-Encoding: gzip, deflate, br, zstd
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Sec-WebSocket-Key: dJtuVuI1PWGVjC2E/qCDbQ==
|
||||
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
|
||||
|
||||
HTTP/1.1 101 Switching Protocols
|
||||
Server: nginx
|
||||
Date: Thu, 03 Jul 2025 04:03:55 GMT
|
||||
Connection: upgrade
|
||||
cache-control: max-age=0, private, must-revalidate
|
||||
sec-websocket-accept: XzR5+Z20bTKH4Ytm23KUTpQmDKE=
|
||||
upgrade: websocket
|
||||
Access-Control-Allow-Origin: *
|
||||
X-Kong-Upstream-Latency: 1
|
||||
X-Kong-Proxy-Latency: 0
|
||||
Via: kong/2.8.1
|
||||
|
||||
|
||||
|
||||
GET wss://ak3.oulog.com/realtime/v1/websocket?apikey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q&vsn=1.0.0 HTTP/1.1
|
||||
Host: ak3.oulog.com
|
||||
Connection: Upgrade
|
||||
Pragma: no-cache
|
||||
Cache-Control: no-cache
|
||||
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1
|
||||
Upgrade: websocket
|
||||
Origin: http://localhost:5173
|
||||
Sec-WebSocket-Version: 13
|
||||
Accept-Encoding: gzip, deflate, br, zstd
|
||||
Accept-Language: zh-CN,zh;q=0.9
|
||||
Sec-WebSocket-Key: ZNkWHFYshDAoPrErr9EY9w==
|
||||
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
|
||||
|
||||
HTTP/1.1 101 Switching Protocols
|
||||
Server: nginx
|
||||
Date: Thu, 03 Jul 2025 07:05:31 GMT
|
||||
Connection: upgrade
|
||||
cache-control: max-age=0, private, must-revalidate
|
||||
sec-websocket-accept: SV8HQ/NAJvS8eQcHVMmIdMRWcb4=
|
||||
upgrade: websocket
|
||||
Access-Control-Allow-Origin: *
|
||||
X-Kong-Upstream-Latency: 3
|
||||
X-Kong-Proxy-Latency: 1
|
||||
Via: kong/2.8.1
|
||||
|
||||
|
||||
|
||||
282
components/supadb/aksuparealtime.uts
Normal file
282
components/supadb/aksuparealtime.uts
Normal file
@@ -0,0 +1,282 @@
|
||||
// Postgres 变更订阅参数类型(强类型导出,便于 UTS Android 复用)
|
||||
export type PostgresChangesSubscribeParams = {
|
||||
event : string;
|
||||
schema : string;
|
||||
table : string;
|
||||
filter ?: string;
|
||||
topic ?: string;
|
||||
onChange : (payload : any) => void;
|
||||
};
|
||||
|
||||
type PostgresChangeListener = {
|
||||
topic : string;
|
||||
event : string;
|
||||
schema : string;
|
||||
table : string;
|
||||
filter : string | null;
|
||||
onChange : (payload : any) => void;
|
||||
};
|
||||
|
||||
export type AkSupaRealtimeOptions = {
|
||||
url : string; // ws/wss 地址
|
||||
channel : string; // 订阅频道
|
||||
token ?: string; // 可选,鉴权token
|
||||
apikey ?: string; // 可选,supabase apikey
|
||||
onMessage : (data : UTSJSONObject) => void;
|
||||
onOpen ?: (res : any) => void;
|
||||
onClose ?: (res : any) => void;
|
||||
onError ?: (err : any) => void;
|
||||
};
|
||||
|
||||
export class AkSupaRealtime {
|
||||
ws : SocketTask | null = null;
|
||||
options : AkSupaRealtimeOptions | null = null;
|
||||
isOpen : boolean = false;
|
||||
heartbeatTimer : any = 0;
|
||||
joinedTopics : Set<string> = new Set<string>();
|
||||
listeners : Array<PostgresChangeListener> = [];
|
||||
|
||||
constructor(options : AkSupaRealtimeOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
connect() {
|
||||
const opts = this.options;
|
||||
if (opts == null) return;
|
||||
// 拼接 apikey 和 vsn=1.0.0 到 ws url
|
||||
let wsUrl = opts.url;
|
||||
// apikey 兼容 query 已有参数和无参数两种情况
|
||||
if (opts.apikey != null && opts.apikey !== "") {
|
||||
const hasQuery = wsUrl.indexOf('?') != -1;
|
||||
// 移除已有 apikey 参数,避免重复
|
||||
wsUrl = wsUrl.replace(/([&?])apikey=[^&]*/g, '$1').replace(/[?&]$/, '');
|
||||
wsUrl += (hasQuery ? '&' : '?') + 'apikey=' + encodeURIComponent('' + opts.apikey);
|
||||
}
|
||||
if (wsUrl.indexOf('vsn=') == -1) {
|
||||
wsUrl += (wsUrl.indexOf('?') == -1 ? '?' : '&') + 'vsn=1.0.0';
|
||||
}
|
||||
this.ws = uni.connectSocket({
|
||||
url: wsUrl,
|
||||
success: (res) => { console.log(res); },
|
||||
fail: (err) => { if (opts.onError != null) opts.onError?.(err); }
|
||||
});
|
||||
if (this.ws != null) {
|
||||
const wsTask = this.ws;
|
||||
wsTask?.onOpen((result : OnSocketOpenCallbackResult) => {
|
||||
this.isOpen = true;
|
||||
console.log('onopen', result)
|
||||
if (opts.onOpen != null) opts.onOpen?.(result);
|
||||
// 启动 heartbeat 定时器
|
||||
this.startHeartbeat();
|
||||
});
|
||||
wsTask?.onMessage((msg) => {
|
||||
console.log(msg)
|
||||
let data : UTSJSONObject | null = null;
|
||||
try {
|
||||
const msgData = (typeof msg == 'object' && msg.data !== null) ? msg.data : msg;
|
||||
data = typeof msgData == 'string' ? JSON.parse(msgData) as UTSJSONObject : msgData as UTSJSONObject;
|
||||
} catch (e) { }
|
||||
// 处理 pong
|
||||
if (
|
||||
data != null &&
|
||||
data.event == 'phx_reply' &&
|
||||
typeof data.payload == 'object' &&
|
||||
data.payload != null &&
|
||||
(data.payload as UTSJSONObject).status != null &&
|
||||
(data.payload as UTSJSONObject).status == 'ok' &&
|
||||
(data.payload as UTSJSONObject).response != null &&
|
||||
(data.payload as UTSJSONObject).response == 'heartbeat'
|
||||
) {
|
||||
// 收到 pong,可用于续约
|
||||
// 可选:重置定时器
|
||||
}
|
||||
console.log(data)
|
||||
if (data != null) this.dispatchPostgresChange(data);
|
||||
if (opts?.onMessage != null) opts.onMessage?.(data ?? ({} as UTSJSONObject));
|
||||
});
|
||||
wsTask?.onClose((res) => {
|
||||
console.log('onclose', res)
|
||||
this.isOpen = false;
|
||||
this.joinedTopics.clear();
|
||||
this.listeners = [];
|
||||
if (opts.onClose != null) opts.onClose?.(res);
|
||||
this.stopHeartbeat();
|
||||
});
|
||||
wsTask?.onError((err) => {
|
||||
console.log(err)
|
||||
if (opts.onError != null) opts.onError?.(err);
|
||||
this.stopHeartbeat();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
send(options : SendSocketMessageOptions) {
|
||||
const wsTask = this.ws;
|
||||
if (wsTask != null && this.isOpen) {
|
||||
console.log('send:', options)
|
||||
// 兼容 uni-app-x send API,支持 success/fail 回调
|
||||
// 只允许 SendSocketMessageOptions 类型,避免 UTSJSONObject 混用
|
||||
let sendData : any = options.data;
|
||||
// 若 data 不是字符串,自动序列化
|
||||
if (typeof sendData !== 'string') {
|
||||
sendData = JSON.stringify(sendData);
|
||||
}
|
||||
options.success ?? ((res) => {
|
||||
if (typeof options.success == 'function') options.success?.(res)
|
||||
})
|
||||
options.fail ?? ((err : any) => {
|
||||
console.log(err)
|
||||
const opts = this.options;
|
||||
if (opts != null && opts.onError != null) opts.onError?.(err);
|
||||
})
|
||||
wsTask.send(options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
close(options : CloseSocketOptions) {
|
||||
this.ws?.close(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅 Postgres 变更事件(如 INSERT/UPDATE/DELETE)
|
||||
* @param params { event: 'INSERT'|'UPDATE'|'DELETE', schema: string, table: string, onChange: (payload: any) => void }
|
||||
*/
|
||||
/**
|
||||
* 订阅 Postgres 变更事件(如 INSERT/UPDATE/DELETE)
|
||||
* @param params { event: 'INSERT'|'UPDATE'|'DELETE', schema: string, table: string, onChange: (payload: any) => void }
|
||||
*/
|
||||
/**
|
||||
* 订阅 Postgres 变更事件(如 INSERT/UPDATE/DELETE)
|
||||
* @param params { event: 'INSERT'|'UPDATE'|'DELETE', schema: string, table: string, onChange: (payload: any) => void }
|
||||
*/
|
||||
/**
|
||||
* 订阅 Postgres 变更事件(如 INSERT/UPDATE/DELETE)
|
||||
* @param params { event: 'INSERT'|'UPDATE'|'DELETE', schema: string, table: string, onChange: (payload: any) => void }
|
||||
*/
|
||||
/**
|
||||
* 订阅 Postgres 变更事件(如 INSERT/UPDATE/DELETE)
|
||||
* @param params { event: 'INSERT'|'UPDATE'|'DELETE', schema: string, table: string, onChange: (payload: any) => void }
|
||||
*/
|
||||
public subscribePostgresChanges(params : PostgresChangesSubscribeParams) : void {
|
||||
const opts = this.options;
|
||||
if (this.isOpen !== true || opts == null) {
|
||||
throw new Error('WebSocket 未连接');
|
||||
}
|
||||
const topic = params.topic != null && params.topic !== '' ? params.topic : `realtime:${params.schema}:${params.table}`;
|
||||
this.joinTopicIfNeeded(topic, params);
|
||||
this.listeners.push({
|
||||
topic: topic,
|
||||
event: params.event,
|
||||
schema: params.schema,
|
||||
table: params.table,
|
||||
filter: params.filter != null ? params.filter : null,
|
||||
onChange: params.onChange
|
||||
});
|
||||
}
|
||||
|
||||
startHeartbeat() {
|
||||
this.stopHeartbeat();
|
||||
console.log('make heartbeat')
|
||||
// 每 30 秒发送一次 heartbeat(官方建议)
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
console.log('should startHeartbeat')
|
||||
if (this.isOpen && this.ws != null) {
|
||||
const heartbeatMsg = {
|
||||
topic: 'phoenix',
|
||||
event: 'heartbeat',
|
||||
payload: {},
|
||||
ref: Date.now().toString()
|
||||
};
|
||||
this.send({ data: JSON.stringify(heartbeatMsg) });
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
stopHeartbeat() {
|
||||
console.log('stop heartbeat')
|
||||
if (typeof this.heartbeatTimer == 'number' && this.heartbeatTimer > 0) {
|
||||
clearInterval(this.heartbeatTimer as number);
|
||||
this.heartbeatTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private joinTopicIfNeeded(topic : string, params : PostgresChangesSubscribeParams) {
|
||||
if (topic == null || topic == '') return;
|
||||
if (this.joinedTopics.has(topic)) return;
|
||||
|
||||
let changeConfig : any = null;
|
||||
if (params.filter != null && params.filter !== '') {
|
||||
changeConfig = {
|
||||
event: params.event,
|
||||
schema: params.schema,
|
||||
table: params.table,
|
||||
filter: params.filter
|
||||
};
|
||||
} else {
|
||||
changeConfig = {
|
||||
event: params.event,
|
||||
schema: params.schema,
|
||||
table: params.table
|
||||
};
|
||||
}
|
||||
|
||||
const joinMsg = {
|
||||
event: 'phx_join',
|
||||
payload: {
|
||||
config: {
|
||||
broadcast: { self: false, ack: false },
|
||||
postgres_changes: [changeConfig],
|
||||
presence: { key: '', enabled: false },
|
||||
private: false
|
||||
},
|
||||
access_token: this.options != null && this.options.token != null ? this.options.token : null
|
||||
},
|
||||
ref: Date.now().toString(),
|
||||
topic: topic
|
||||
};
|
||||
this.send({ data: JSON.stringify(joinMsg) });
|
||||
this.joinedTopics.add(topic);
|
||||
}
|
||||
|
||||
private dispatchPostgresChange(data : UTSJSONObject) : void {
|
||||
if (data.event !== 'postgres_changes') return;
|
||||
const topic = typeof data.topic == 'string' ? data.topic : '';
|
||||
const payload = data.payload as UTSJSONObject | null;
|
||||
if (payload == null) return;
|
||||
const dataSection = payload.get('data') as UTSJSONObject | null;
|
||||
let payloadEvent = payload.getString('event') as string | null;
|
||||
if ((payloadEvent == null || payloadEvent == '') && dataSection != null) {
|
||||
const typeValue = dataSection.getString('type') as string | null;
|
||||
if (typeValue != null && typeValue !== '') payloadEvent = typeValue;
|
||||
}
|
||||
let schemaName = payload.getString('schema') as string | null;
|
||||
if ((schemaName == null || schemaName == '') && dataSection != null) {
|
||||
const dataSchema = dataSection.getString('schema') as string | null;
|
||||
if (dataSchema != null && dataSchema !== '') schemaName = dataSchema;
|
||||
}
|
||||
let tableName = payload.getString('table') as string | null;
|
||||
if ((tableName == null || tableName == '') && dataSection != null) {
|
||||
const dataTable = dataSection.getString('table') as string | null;
|
||||
if (dataTable != null && dataTable !== '') tableName = dataTable;
|
||||
}
|
||||
const filterValue = payload.getString('filter') as string | null;
|
||||
for (let i = 0; i < this.listeners.length; i++) {
|
||||
const listener = this.listeners[i];
|
||||
if (listener.topic !== topic) continue;
|
||||
if (listener.event !== '*' && payloadEvent != null && listener.event !== payloadEvent) continue;
|
||||
if (schemaName != null && listener.schema !== schemaName) continue;
|
||||
if (tableName != null && listener.table !== tableName) continue;
|
||||
if (
|
||||
listener.filter != null && listener.filter !== '' &&
|
||||
filterValue != null && listener.filter !== filterValue
|
||||
) continue;
|
||||
if (typeof listener.onChange == 'function') {
|
||||
const changeData = dataSection != null ? dataSection : payload;
|
||||
listener.onChange(changeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AkSupaRealtime;
|
||||
36
components/supadb/rag.uts
Normal file
36
components/supadb/rag.uts
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
// const agent_id = "2bb9fa0cbae011efbf780242ac120006";
|
||||
const agent_id = "15b01b26128111f08cd30242ac120006";
|
||||
// const agent_id = "9eb32c5395d64ac48752b25efdd3b3bb";
|
||||
// const requrl = "https://rag.oulog.com/v1/canvas/completion";
|
||||
const requrl = "https://rag.oulog.com/api/v1/agents_openai/"+agent_id+"/chat/completions";
|
||||
// let beareaToken = "ImQwODRkOGJlZjI3ZjExZWZhZTZhMDI0MmFjMTIwMDA2Ig.Z7wduA.DEPPVfSZaP2MBKJN8vw14VxOXG0";
|
||||
import { RAG_API_KEY } from "@/ak/config";
|
||||
let beareaToken = RAG_API_KEY
|
||||
export function requestCanvasCompletion(question) {
|
||||
const new_uuid = `${Date.now()}${Math.floor(Math.random() * 1e7)}`
|
||||
const messages = [{"role": "user", "content": question}]
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
url: requrl,
|
||||
method: "POST",
|
||||
data: {
|
||||
id: agent_id,
|
||||
messages: messages,
|
||||
stream: false,
|
||||
model:"deepseek-r1",
|
||||
message_id: new_uuid,
|
||||
},
|
||||
header: {
|
||||
"content-Type": "application/json",
|
||||
Authorization: 'Bearer '+beareaToken,
|
||||
},
|
||||
success: (res) => {
|
||||
resolve(res.data);
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
7
components/supadb/raginstance.uts
Normal file
7
components/supadb/raginstance.uts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { RagReq,RagReqConfig } from '@/uni_modules/rag-req/rag-req.uts'
|
||||
import { RAG_BASE_URL, RAG_API_KEY } from '@/ak/config.uts'
|
||||
|
||||
const ragconfig = { baseUrl: RAG_BASE_URL, apiKey: RAG_API_KEY } as RagReqConfig
|
||||
const rag = new RagReq(ragconfig)
|
||||
|
||||
export default rag
|
||||
364
components/supadb/supadb.uvue
Normal file
364
components/supadb/supadb.uvue
Normal file
@@ -0,0 +1,364 @@
|
||||
<template>
|
||||
<slot name="default" :data="data" :current="localPageCurrent" :total="total" :hasmore="hasmore" :loading="loading" :error="error">
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts"> import { ref, watch, onMounted } from 'vue';
|
||||
import supa from './aksupainstance.uts';
|
||||
import { AkSupaSelectOptions } from './aksupa.uts'
|
||||
import { AkReqResponse } from '@/uni_modules/ak-req/index.uts';
|
||||
import { toUniError } from '@/utils/utils.uts';
|
||||
const props = defineProps({
|
||||
collection: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
filter: {
|
||||
type: UTSJSONObject,
|
||||
default: () => ({}),
|
||||
},
|
||||
field: {
|
||||
type: String,
|
||||
default: '*'
|
||||
},
|
||||
where: Object,
|
||||
orderby: String,
|
||||
pageData: {
|
||||
type: String,
|
||||
default: 'add',
|
||||
},
|
||||
pageCurrent: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
/*"exact" | "planned" | "estimated" */
|
||||
getcount: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
getone: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadtime: {
|
||||
type: String,
|
||||
default: 'auto',
|
||||
},
|
||||
datafunc: Function,
|
||||
// RPC 函数名,当使用 RPC 时,collection 参数可以为空
|
||||
rpc: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// RPC 参数,用于传递给 RPC 函数的额外参数
|
||||
params: {
|
||||
type: UTSJSONObject,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
const emit = defineEmits<{
|
||||
(e : 'process-data', val : UTSJSONObject) : void,
|
||||
(e : 'load', val : any[]) : void,
|
||||
(e : 'error', val : any) : void
|
||||
}>();
|
||||
|
||||
const data = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref<any>('');
|
||||
const total = ref(0);
|
||||
const hasmore = ref(true);
|
||||
|
||||
// Use local refs for pagination state to avoid mutating props directly
|
||||
const localPageCurrent = ref(props.pageCurrent);
|
||||
const localPageSize = ref(props.pageSize);
|
||||
|
||||
type Pagination = {
|
||||
count : Number;
|
||||
current : Number;
|
||||
size : Number;
|
||||
};
|
||||
|
||||
let pagination = { total: 0 };
|
||||
|
||||
let hasMoreData = true;
|
||||
|
||||
|
||||
/**
|
||||
* Unified data loading method
|
||||
* @param {UTSJSONObject} opt
|
||||
* opt.append Whether to append data
|
||||
* opt.clear Whether to clear data
|
||||
* opt.page Specify page number
|
||||
*/ const fetchData = async (opt : UTSJSONObject) => {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
|
||||
// 检查是否为 RPC 调用
|
||||
const isRpcCall = props.rpc != null && props.rpc.length > 0;
|
||||
|
||||
// 只有在非 RPC 调用时才检查 collection
|
||||
if (!isRpcCall && (props.collection == null || props.collection.trim() == '')) {
|
||||
error.value = 'collection/table 不能为空';
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// RPC 调用时检查 rpc 参数
|
||||
if (isRpcCall && (props.rpc == null || props.rpc.trim() == '')) {
|
||||
error.value = 'rpc 函数名不能为空';
|
||||
loading.value = false;
|
||||
return;
|
||||
}try {
|
||||
// Platform-specific parameter extraction for UTSJSONObject compatibility
|
||||
let append: boolean = false
|
||||
let clear: boolean = false
|
||||
let page: number = localPageCurrent.value
|
||||
|
||||
// #ifdef APP-ANDROID || APP-IOS
|
||||
// Native platform: use UTSJSONObject methods
|
||||
append = opt.getBoolean('append') ?? false
|
||||
clear = opt.getBoolean('clear') ?? false
|
||||
page = opt.getNumber('page') ?? localPageCurrent.value
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID || APP-IOS
|
||||
// Web platform: direct property access
|
||||
append = (opt as any)['append'] as boolean ?? false
|
||||
clear = (opt as any)['clear'] as boolean ?? false
|
||||
page = (opt as any)['page'] as number ?? localPageCurrent.value
|
||||
// #endif
|
||||
|
||||
// Update local pagination state
|
||||
localPageCurrent.value = page;
|
||||
localPageSize.value = props.pageSize;
|
||||
|
||||
// Build query options
|
||||
let selectOptions : AkSupaSelectOptions = {
|
||||
limit: localPageSize.value,
|
||||
order: props.orderby,
|
||||
columns: props.field,
|
||||
};
|
||||
if (props.getcount != null && props.getcount.length > 0) {
|
||||
selectOptions['getcount'] = props.getcount;
|
||||
} let result: any;
|
||||
if (isRpcCall) {
|
||||
// 支持rpc调用 - RPC方法只接受functionName和params两个参数
|
||||
// 将filter、params和selectOptions合并为rpcParams
|
||||
const rpcParams = new UTSJSONObject();
|
||||
|
||||
// 首先添加props.params中的参数
|
||||
if (props.params != null) {
|
||||
const paramsKeys = UTSJSONObject.keys(props.params);
|
||||
for (let i = 0; i < paramsKeys.length; i++) {
|
||||
const key = paramsKeys[i];
|
||||
|
||||
// #ifdef APP-ANDROID || APP-IOS
|
||||
// Native platform: use UTSJSONObject methods
|
||||
rpcParams.set(key, props.params.get(key));
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID || APP-IOS
|
||||
// Web platform: direct property access
|
||||
rpcParams.set(key, (props.params as any)[key]);
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
// 然后添加filter中的参数(可能会覆盖params中的同名参数)
|
||||
if (props.filter != null) {
|
||||
// Platform-specific filter handling for UTSJSONObject compatibility
|
||||
const filterKeys = UTSJSONObject.keys(props.filter);
|
||||
for (let i = 0; i < filterKeys.length; i++) {
|
||||
const key = filterKeys[i];
|
||||
|
||||
// #ifdef APP-ANDROID || APP-IOS
|
||||
// Native platform: use UTSJSONObject methods
|
||||
rpcParams.set(key, props.filter.get(key));
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID || APP-IOS
|
||||
// Web platform: direct property access
|
||||
rpcParams.set(key, (props.filter as any)[key]);
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
// 添加分页和排序参数
|
||||
if (selectOptions.limit != null) rpcParams.set('limit', selectOptions.limit);
|
||||
if (selectOptions.order != null) rpcParams.set('order', selectOptions.order);
|
||||
if (selectOptions.columns != null) rpcParams.set('columns', selectOptions.columns);
|
||||
if (selectOptions.getcount != null) rpcParams.set('getcount', selectOptions.getcount);
|
||||
|
||||
result = await supa.rpc(props.rpc, rpcParams);
|
||||
} else {
|
||||
// Query data
|
||||
result = await supa.select_uts(props.collection, props.filter, selectOptions);
|
||||
}
|
||||
// headers 判空
|
||||
let countstring = '';
|
||||
let headers:UTSJSONObject = result.headers != null ? result.headers : {};
|
||||
if (headers != null) {
|
||||
if (typeof headers.getString == 'function') {
|
||||
let val = headers.getString('content-range');
|
||||
if (val != null && typeof val == 'string') {
|
||||
countstring = val;
|
||||
}
|
||||
} else if (headers['content-range'] != null) {
|
||||
// 类型断言为 string,否则转为 string
|
||||
countstring = `${headers['content-range']}`;
|
||||
}
|
||||
}
|
||||
console.log(countstring)
|
||||
if (countstring != null && countstring != '') {
|
||||
try {
|
||||
const rangeParts = countstring.split('/')
|
||||
if (rangeParts.length == 2) {
|
||||
// 检查第二部分是否为数字(不是 '*')
|
||||
const totalPart = rangeParts[1].trim()
|
||||
if (totalPart !== '*' && !isNaN(parseInt(totalPart))) {
|
||||
total.value = parseInt(totalPart)
|
||||
console.log('Total count from header:', total.value)
|
||||
pagination.total = total.value;
|
||||
|
||||
const rangeValues = rangeParts[0].split('-')
|
||||
if (rangeValues.length == 2) {
|
||||
const end = parseInt(rangeValues[1])
|
||||
hasmore.value = end < total.value - 1
|
||||
hasMoreData = hasmore.value
|
||||
} } else {
|
||||
// 当总数未知时(返回 *),设置默认值
|
||||
console.log('Total count unknown (*), using default pagination logic')
|
||||
total.value = 0
|
||||
pagination.total = 0
|
||||
// 根据当前返回的数据量判断是否还有更多数据
|
||||
hasmore.value = Array.isArray(result.data) && (result.data as any[]).length >= localPageSize.value
|
||||
hasMoreData = hasmore.value
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse content-range header', e)
|
||||
} } else {
|
||||
// 当没有 content-range header 时,根据返回的数据量判断分页
|
||||
console.log('No content-range header, using data length for pagination')
|
||||
total.value = 0
|
||||
pagination.total = 0
|
||||
// 如果返回的数据量等于页面大小,可能还有更多数据
|
||||
hasmore.value = Array.isArray(result.data) && (result.data as any[]).length >= localPageSize.value
|
||||
hasMoreData = hasmore.value
|
||||
}
|
||||
// data 判空
|
||||
// UTS 平台下无 UTSArray.fromAny,需兼容
|
||||
let items: Array<any> = (Array.isArray(result.data)) ? result.data as Array<any> : Array<any>();
|
||||
try {
|
||||
// emit('process-data', items != null ? items : []);
|
||||
console.log(result)
|
||||
|
||||
// Manually create UTSJSONObject from AkReqResponse for cross-platform compatibility
|
||||
const prodata = new UTSJSONObject()
|
||||
prodata.set('status', result.status)
|
||||
prodata.set('data', result.data)
|
||||
prodata.set('headers', result.headers)
|
||||
prodata.set('error', result.error)
|
||||
prodata.set('total', total.value)
|
||||
|
||||
emit('process-data', prodata);
|
||||
} catch (e) {
|
||||
console.error('emit process-data error', e);
|
||||
}
|
||||
|
||||
if (clear) {
|
||||
data.value = [];
|
||||
}
|
||||
if (append) {
|
||||
if(Array.isArray(items)) {
|
||||
data.value = ([] as any[]).concat(data.value, items);
|
||||
}
|
||||
} else {
|
||||
data.value = items as any[];
|
||||
}
|
||||
pagination.total = total.value;
|
||||
hasMoreData = Array.isArray(items) && items.length == localPageSize.value; } catch (err : any) {
|
||||
// 使用标准化错误处理
|
||||
const uniError = toUniError(err, 'An error occurred while fetching data')
|
||||
|
||||
try {
|
||||
emit('error', uniError);
|
||||
} catch (e) {
|
||||
console.error('emit error event error', e);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 页码变更统一通过 fetchData
|
||||
const handlePageChange = (page : number) => {
|
||||
fetchData({ page: page, clear: true, append: false } as UTSJSONObject);
|
||||
};
|
||||
// 主动加载数据,支持清空
|
||||
const loadData = (opt : UTSJSONObject) => {
|
||||
console.log('loadData')
|
||||
|
||||
// Platform-specific parameter extraction for UTSJSONObject compatibility
|
||||
let clear: boolean = true
|
||||
|
||||
// #ifdef APP-ANDROID || APP-IOS
|
||||
// Native platform: use UTSJSONObject methods
|
||||
clear = opt.getBoolean('clear') ?? true
|
||||
// #endif
|
||||
|
||||
// #ifndef APP-ANDROID || APP-IOS
|
||||
// Web platform: direct property access
|
||||
clear = (opt as any)['clear'] as boolean ?? true
|
||||
// #endif
|
||||
|
||||
fetchData({ clear, append: false, page: 1 } as UTSJSONObject);
|
||||
};
|
||||
|
||||
const refresh = () => {
|
||||
console.log('refresh')
|
||||
const clear = true;
|
||||
fetchData({ clear, append: false, page: 1 } as UTSJSONObject);
|
||||
};
|
||||
|
||||
// 加载更多,自动追加
|
||||
const loadMore = () => {
|
||||
if (hasMoreData) {
|
||||
const nextPage = props.pageCurrent + 1;
|
||||
fetchData({ append: true, clear: false, page: nextPage } as UTSJSONObject);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (props.loadtime === 'auto' || props.loadtime === 'onready') {
|
||||
onMounted(() => {
|
||||
fetchData({} as UTSJSONObject);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// watch(data, (newValue : any) => {
|
||||
// emit('load', newValue);
|
||||
// });
|
||||
watch(error, (newValue : any) => {
|
||||
if (newValue != null) {
|
||||
emit('error', newValue);
|
||||
}
|
||||
});
|
||||
// Documented exposed methods for parent components
|
||||
/**
|
||||
* Exposed methods:
|
||||
* - loadData(opt?): Load data, optionally clearing previous data
|
||||
* - loadMore(): Load next page and append
|
||||
*/
|
||||
defineExpose({ loadData,refresh, loadMore,hasmore,total });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 添加您的样式 */
|
||||
</style>
|
||||
122
components/supadb/typed-examples.uts
Normal file
122
components/supadb/typed-examples.uts
Normal file
@@ -0,0 +1,122 @@
|
||||
// 示例:如何使用 AkSupa 的 executeAs<T>() 类型转换功能
|
||||
|
||||
// 定义数据类型
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
created_at: string;
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
export type Post = {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
user_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
import AkSupa from '@/components/supadb/aksupa.uts';
|
||||
|
||||
export async function demonstrateTypedQueries() {
|
||||
const supa = new AkSupa('https://your-project.supabase.co', 'your-anon-key');
|
||||
|
||||
// 1. 查询数据 - 使用链式调用 + executeAs<T>()
|
||||
const usersResult = await supa
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('status', 'active')
|
||||
.limit(10)
|
||||
.executeAs<User[]>();
|
||||
|
||||
// 现在 usersResult.data 是 User[] 类型,而不是 UTSJSONObject
|
||||
if (usersResult.data != null) {
|
||||
usersResult.data.forEach(user => {
|
||||
console.log(`用户: ${user.name}, 邮箱: ${user.email}`);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 单条记录查询
|
||||
const userResult = await supa
|
||||
.from('users')
|
||||
.select('*')
|
||||
.eq('id', 1)
|
||||
.single()
|
||||
.executeAs<User>();
|
||||
|
||||
if (userResult.data != null) {
|
||||
console.log(`用户名: ${userResult.data.name}`);
|
||||
}
|
||||
|
||||
// 3. 插入数据
|
||||
const newUser = {
|
||||
name: '新用户',
|
||||
email: 'newuser@example.com'
|
||||
} as UTSJSONObject;
|
||||
|
||||
const insertResult = await supa
|
||||
.from('users')
|
||||
.insert(newUser)
|
||||
.executeAs<User[]>();
|
||||
|
||||
// 4. 更新数据
|
||||
const updateResult = await supa
|
||||
.from('users')
|
||||
.update({ name: '更新的名称' } as UTSJSONObject)
|
||||
.eq('id', 1)
|
||||
.executeAs<User[]>();
|
||||
|
||||
// 5. 删除数据
|
||||
const deleteResult = await supa
|
||||
.from('users')
|
||||
.delete()
|
||||
.eq('id', 1)
|
||||
.executeAs<User[]>();
|
||||
|
||||
// 6. RPC 调用
|
||||
const rpcResult = await supa
|
||||
.from('') // RPC 不需要 table
|
||||
.rpc('get_user_stats', { user_id: 1 } as UTSJSONObject)
|
||||
.executeAs<{ total_posts: number; total_likes: number }>();
|
||||
|
||||
// 7. 复杂查询示例
|
||||
const complexQuery = await supa
|
||||
.from('posts')
|
||||
.select('*, users!posts_user_id_fkey(*)')
|
||||
.eq('status', 'published')
|
||||
.gt('created_at', '2024-01-01')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(20)
|
||||
.executeAs<Post[]>();
|
||||
|
||||
return {
|
||||
users: usersResult.data,
|
||||
user: userResult.data,
|
||||
newUser: insertResult.data,
|
||||
updated: updateResult.data,
|
||||
deleted: deleteResult.data,
|
||||
stats: rpcResult.data,
|
||||
posts: complexQuery.data
|
||||
};
|
||||
}
|
||||
|
||||
// 平台兼容性说明:
|
||||
//
|
||||
// Android 平台(uni-app x 3.90+):
|
||||
// - 使用 UTSJSONObject.parse() 进行真正的类型转换
|
||||
// - 数据会被正确解析为指定的类型 T
|
||||
// - 如果转换失败,会 fallback 到原始数据
|
||||
//
|
||||
// 其他平台(Web、iOS、HarmonyOS):
|
||||
// - 使用 as 进行类型断言
|
||||
// - 这只是 TypeScript 编译时的类型提示,运行时仍然是原始数据
|
||||
// - 但提供了更好的开发体验和类型安全
|
||||
//
|
||||
// 使用优势:
|
||||
// 1. 统一的 API - 只需要记住 executeAs<T>() 一个方法
|
||||
// 2. 链式调用 - 可以和所有其他方法组合使用
|
||||
// 3. 类型安全 - 编译时类型检查,运行时类型转换(Android)
|
||||
// 4. 简洁明了 - 不需要多个 selectAs/insertAs/updateAs 等方法
|
||||
Reference in New Issue
Block a user