diff --git a/ak/config.uts b/ak/config.uts index 5456d84c..87731945 100644 --- a/ak/config.uts +++ b/ak/config.uts @@ -1,20 +1,16 @@ // Supabase 配置 // 内网环境 - 本地部署的 Supabase // IP: 192.168.1.62 -// IP: 192.168.1.62 // Kong HTTP Port: 8000 -//自己的配置自己解开即可 + +//export const SUPA_URL: string = 'http://192.168.1.61:18000' +//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' export const SUPA_URL: string = 'http://192.168.1.61:18000' export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' -//export const SUPA_URL: string = 'http://192.168.1.62:18000' -//export const SUPA_KEY: string = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlLTEiLCJpYXQiOjE3Njk2NzY0OTgsImV4cCI6MTkyNzM1NjQ5OH0.ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' -//export const SUPA_URL: string = 'http://192.168.1.63:18000' -//export const SUPA_KEY: string = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJyb2xlIjogImFub24iLCAiaXNzIjogInN1cGFiYXNlIiwgImlhdCI6IDE3Njk4NDczMzQsICJleHAiOiAyMDg1MjA3MzM0fQ.js-2CS5_cUmf4iVv8aCmmx9iyFsQvLNDbt8YYOngeLU' // WebSocket 实时连接(内网使用 ws:// 而非 wss://) export const WS_URL: string = 'ws://192.168.1.61:18000/realtime/v1/websocket' -//export const WS_URL: string = 'ws://192.168.1.62:18000/realtime/v1/websocket' -//export const WS_URL: string = 'ws://192.168.1.63:18000/realtime/v1/websocket' +//export const WS_URL: string = 'ws://localhost:18000/realtime/v1/websocket' // 备用配置(已注释,如需切换可取消注释) // 开发环境 - 其他内网地址 @@ -37,4 +33,4 @@ export const HOME_REDIRECT: string = '/pages/mall/consumer/index' export const TABORPAGE: string = '/pages/mall/consumer/index' // 测试模式:放开任意跳转(禁用启动页/登录/401 的强制重定向) -export const IS_TEST_MODE: boolean = true +export const IS_TEST_MODE: boolean = true \ No newline at end of file diff --git a/components/analytics/AnalyticsRegionMap.uvue b/components/analytics/AnalyticsRegionMap.uvue index 6b98d489..77720af7 100644 --- a/components/analytics/AnalyticsRegionMap.uvue +++ b/components/analytics/AnalyticsRegionMap.uvue @@ -331,7 +331,7 @@ export default { .map-title { font-size: 14px; - font-weight: 600; + font-weight: 700; color: #111; } diff --git a/components/analytics/AnalyticsSidebarMenu.uvue b/components/analytics/AnalyticsSidebarMenu.uvue index a0d6c18b..a5b19c01 100644 --- a/components/analytics/AnalyticsSidebarMenu.uvue +++ b/components/analytics/AnalyticsSidebarMenu.uvue @@ -394,7 +394,7 @@ export default { .menu-item.active .menu-text { color: #3b82f6; - font-weight: 600; + font-weight: 700; } .menu-icon { diff --git a/components/supadb/aksupa.uts b/components/supadb/aksupa.uts index 80255cf6..c107e8b8 100644 --- a/components/supadb/aksupa.uts +++ b/components/supadb/aksupa.uts @@ -79,11 +79,11 @@ export class AkSupaQueryBuilder { like(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'like', value); } ilike(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'ilike', value); } in(field : string, value : any[]) : AkSupaQueryBuilder { return this._addCond(field, 'in', value); } - is(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'is', value); } + is(field : string, value : any | null) : AkSupaQueryBuilder { return this._addCond(field, 'is', value); } contains(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cs', value); } containedBy(field : string, value : any) : AkSupaQueryBuilder { return this._addCond(field, 'cd', value); } - not(field : string, opOrValue : any, value?: any) : AkSupaQueryBuilder { - if (value !== undefined) { + not(field : string, opOrValue : any, value: any | null = null) : AkSupaQueryBuilder { + if (value != null) { // 三元形式:field, operator, value // 例如 not('badge', 'is', null) -> badge=not.is.null const combinedOp = 'not.' + opOrValue; @@ -113,7 +113,7 @@ export class AkSupaQueryBuilder { return this; } - private _addCond(afield : string, op : string, value : any) : AkSupaQueryBuilder { + private _addCond(afield : string, op : string, value : any | null) : AkSupaQueryBuilder { //console.log('add cond:', op, afield, value) const field = encodeURIComponent(afield)!! // 将 null 转换为字符串 'null',避免构造对象时缺少 value 属性 @@ -266,7 +266,7 @@ export class AkSupaQueryBuilder { return params.length > 0 ? params.join('&') : null; } - select(columns ?: string, opt ?: UTSJSONObject) : AkSupaQueryBuilder { + select(columns : string = "*", opt : UTSJSONObject | null = null) : AkSupaQueryBuilder { this._action = 'select'; if (columns != null) { this._options.columns = columns; @@ -692,7 +692,7 @@ export class AkSupa { token_type: 'bearer', expires_in: 0, raw: user - } as any; + } as AkSupaSignInResult; } return true; } catch (e) { @@ -999,6 +999,22 @@ async delete(table : string, filter : string | null) : Promise { + channel.unsubscribe(); + return Promise.resolve('ok'); + } // AkSupa类内新增:自动刷新session async refreshSession() : Promise { if (this.session == null || this.session?.refresh_token == null) return false; @@ -1133,4 +1149,122 @@ export function createClient(url : string, key : string) : AkSupa { return new AkSupa(url, key); } +// 模拟 Realtime Channel 类 (Polling Fallback) +export class AkSupaRealtimeChannel { + private _supa: AkSupa; + private _topic: string; + private _timer: number = 0; + private _callback: ((payload: any) => void) | null = null; + private _table: string = ''; + private _lastTime: string = new Date().toISOString(); + private _isSubscribed: boolean = false; + + constructor(supa: AkSupa, topic: string) { + this._supa = supa; + this._topic = topic; + } + + // 绑定事件 (仅支持 postgres_changes INSERT) + on(type: string, filter: UTSJSONObject, callback: (payload: any) => void): AkSupaRealtimeChannel { + // 解析 table + const table = filter.getString('table'); + if (table != null) { + this._table = table; + } + this._callback = callback; + return this; + } + + // 开始订阅 + subscribe(callback?: (status: string, err: any | null) => void): AkSupaRealtimeChannel { + if (this._isSubscribed) return this; + this._isSubscribed = true; + + // 初始回调 + if (callback != null) { + callback('SUBSCRIBED', null); + } + + // 如果没有指定 table,无法轮询 + if (this._table == '') { + console.warn('Realtime check: No table specified for polling.'); + return this; + } + + // 开始轮询 (每3秒) + this._timer = setInterval(() => { + this._checkUpdates(); + }, 3000); + + return this; + } + + // 停止订阅 + unsubscribe() { + if (this._timer > 0) { + clearInterval(this._timer); + this._timer = 0; + } + this._isSubscribed = false; + } + + // 检查更新 + private async _checkUpdates() { + if (!this._isSubscribed || this._table == '') return; + + try { + const now = new Date().toISOString(); + + const res = await this._supa + .from(this._table) + .select('*') + .gt('created_at', this._lastTime) + .order('created_at', { ascending: true }) + .execute(); + + if (res.error == null && res.data != null) { + let list: any[] = []; + if (Array.isArray(res.data)) { + list = res.data as any[]; + } + + if (list.length > 0) { + // 更新最后时间 + const lastItem = list[list.length - 1]; + let lastTimeStr: string | null = null; + + if (lastItem instanceof UTSJSONObject) { + lastTimeStr = lastItem.getString('created_at'); + } else { + // 尝试转 json + const j = JSON.parse(JSON.stringify(lastItem)) as UTSJSONObject; + lastTimeStr = j.getString('created_at'); + } + + if (lastTimeStr != null) { + this._lastTime = lastTimeStr; + } else { + this._lastTime = now; + } + + // 触发回调 + if (this._callback != null) { + // 模拟 Realtime payload + list.forEach(item => { + const payload = { + new: item, + eventType: 'INSERT', + old: null + }; + this._callback?.(payload); + }); + } + } + } + } catch (e) { + console.error('Realtime polling error:', e); + } + } +} + export default AkSupa; diff --git a/doc_mall/consumer/ed.md b/doc_mall/consumer/ed.md new file mode 100644 index 00000000..2e18fd12 --- /dev/null +++ b/doc_mall/consumer/ed.md @@ -0,0 +1,119 @@ +把roles/index.uvue 像 users/detail.uvue那样 使用supa 和 store,现在的使用方式幼替换,interface 需要抽出来放到 admins/admintypes.uts并引入。 +参考user-management的onResize处理大小屏的切换问题。 +切换到小屏,card-view的display還是none +超出界面還是需要上下滾動的 + + +https://gitee.com/xiangyuecn/AreaCity-JsSpider-StatsGov + +【在线测试和预览】省市区乡镇四级行政区划数据:支持在线生成JSON、多级联动js; +【在线测试和预览】ECharts Map四级下钻在线测试和预览+代码生成:坐标边界范围在线测试和预览; +【转换工具】AreaCity-Geo格式转换工具软件:csv文件导入数据库,坐标、边界范围转换(支持转成sql、shp、geojson); +【查询工具】AreaCity-Query-Geometry:高性能的坐标数据、边界数据查询工具,Java开源程序、带http查询接口,内存占用低(1秒可查1万个以上坐标对应的城市信息)。 +数据下载 + + +emqx ctl admins add akoo Hexiaoniu1! + + +node.js + fastify + postgres + kafka + +在pages/sports的目录下创建老师和学生端的关于训练提升系统的页面,用严格的uni-app-x的android能运行的模式,使用supadb做aksupainstance完成数据交互部分。同时还需要兼顾 大屏和小屏的不同展现方式. + +我们的i项目是uni-app-x,不是uni-app.目前不支持 uni-easyinput,uni-nav-bar,uni-data-select,uni-icons, uni-datetime-picker。easyinput用input 代替,uni-nav-bar先删除,uni-data-select用picker-view代替,uni-datetime-picker 用components/picker-date 或components/picker-time代替 + +supadb的使用方式是: +// supadb 表格组件实例引用 +const rolesdb = ref(null) + if( rolesdb.value!=null) + { + const rolesdbloaddata = rolesdb.value!!.loadData; + rolesdbloaddata({ clear: false }) + } + + 要获取 後續數據用 emit('load', newValue) 獲取的數據; + +## UTS-Android 兼容性开发规范 + +> 以下为 uni-app-x (UTS) Android 端开发常见注意事项与踩坑点,建议所有开发成员遵循: + +Uts需要注意的几点: + 表单优先用form. + 跟template交互的变量尽量用1维的变量 + 不要用foreach,不要用map,不要用safeget,只要utsjsonobject就好了 + 都用utsjsonobject 了,把safeget 这些必要性不大的函数都去掉,以utsjsonobject为核心 + 用for解析的数组,最好用Array .不要用简写[] + uts android不支持picker,用picker-view,或者uni.showactionsheet.一维的优先用uni.showactionsheet. + uts android不支持undefined + uts 中的变量声明使用 let 和 const,不能使用 var。 + uts 对判断只接受boolean类型,所以要用 !==null之类的来判断空 + uts不支持 Intersection Type is not supported. + uts 中,只适合转 type,不适合使用 interface。因为 interface 在 kotlin 和 swift 中另有不同。 + for 循环里面的i要指定类型为Int ,如:for (let i:Int = 0; i < byteLen; i++) + ||表示逻辑或,&&表示逻辑与,!表示逻辑非,??表示空值合并运算符(当左侧为null或undefined时返回右侧值) + uts在 if里面只接受 boolean类型的值,不能是其他类型的值,否则会报错。 + Index Signature is not supported + uts不支持undefined类型,undefined类型的变量会被当做null处理。 + uts android对map的支持不够好,用for这种方式来代替。 + uts android 对类型要求比较严格,对属性的推断也比较严格,多重推断用as ...来确认类型, + css只支持部分属性,不能使用复杂的选择器和伪类。 + 只支持display: flex; + 不支持gap; + 不支持 display:grid; + 不支持 calc(); + scroll-view 在 uni-app-x中不是用 scroll-y=true 。而是要用 direction="vertical" + + 不支持table,grid,grid-template-columns; + property value `calc(33.33% - 10px)` is not supported for `min-width` (supported values are: `number`|`pixel`)​ + ts的 为空则使用默认值的语法在uts中不能用 ||,要用 ?? 来代替,但不要直接全部替换,因为逻辑与还是 ||。 + uts android不支持!在变量前面的判断空方式,要用 != null 来代替。 + picker-view的事件 UniPickerViewChangeEvent + css: + [APP-ANDROID] 不支持伪类选择器 +[APP-IOS] 不支持伪类选择器 +[APP-ANDROID] 不支持的单位: 'vh' +[APP-IOS] 不支持的单位: 'vh' +css不支持 gap; +WARNING: `backdrop-filter` is not a standard property name (may not be supported)​ + ERROR: Selector `.login-button[disabled]` is not supported. uvue only support classname selector​ + ERROR: property value `100%` is not supported for `min-height` (supported values are: `number`|`pixel`)​ +[plugin:uni:app-uvue-css] ERROR: property value `all` is not supported for `transition-property` (supported values are: `css property`)​ +用 utils/utis 下的 UTSJSONObject 做类型转换 +时间获取用uni_modules/lime-date-time-picker +在script里面或者uts里面要用 import { tt } from '@/utils/i18nfun.uts', +uni-app-x的 android的setup模式下,对顺序有要求, 函数必须放在调用的函数之前,否则无法找到。 +一般情况下,尽可能用强类型模式,uni_modules的情况下,尽量把type定义到interface里面。 +style property `white-space` is only supported on `|